public void TestExpandableIntervalCombinations() { var f0 = new Filling("0", new[] { true }); var f01 = new Filling("01", new[] { true, true }); var f1 = new Filling("1", new[] { false, true }); var f12 = new Filling("12", new[] { false, true, true }); var f02 = new Filling("02", new[] { true, false, true }); var f2 = new Filling("2", new[] { false, false, true }); CanExpandFillingDelegate <Filling> canExpand = (filling, gap) => filling.Occupation.Count >= 2 && filling.Occupation[1] && !filling.Occupation[0] && gap == new Gap(0, 1); IEnumerable <CombinationWithGaps <Filling> > result = Combinatorics.AllFillingIntervalCombinations(f0.ToSingletonReadOnlyList(), 1, canExpand).ToList(); IEnumerable <Filling[]> expectation = new Filling[][] { new Filling[] { f0 } }.ToList(); Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(f1.ToSingletonReadOnlyList(), 2, canExpand).ToList(); expectation = new Filling[][] { new Filling[] { f1 } }.ToList(); //even though f1 doesn't fill position 0 Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(f1.ToSingletonReadOnlyList(), 3, canExpand).ToList(); expectation = new Filling[0][].ToList(); //f1 may expand to position 0, but not to position 2 (which wasn't available in the case above Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(f12.ToSingletonReadOnlyList(), 3, canExpand).ToList(); expectation = new Filling[][] { new Filling[] { f12 } }.ToList(); //even though f12 doesn't fill position 0 Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(new[] { f1, f2 }, 3, canExpand).ToList(); expectation = new Filling[][] { new Filling[] { f1, f2 } }.ToList(); //f1 fills position 0 Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(new[] { f0, f2 }, 3, canExpand).ToList(); expectation = new Filling[0][].ToList(); //position 1 is unfilled Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(new Filling[0], 1, canExpand).ToList(); expectation = new Filling[0][].ToList(); Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(new Filling[0], 3, canExpand).ToList(); expectation = new Filling[0][].ToList(); Contract.Assert(result.ContainsSameElements(expectation, Equals)); result = Combinatorics.AllFillingIntervalCombinations(new[] { f1, f12 }, 3, canExpand).ToList(); expectation = new Filling[][] { new Filling[] { f12 } }.ToList(); //f12 fills position 0, f1 remains unused Contract.Assert(result.ContainsSameElements(expectation, Equals)); //realization: an expansion shouldn't be applied when it could be omitted. Or at least, those without the expansion should be yielded first. //So the output of AllFillingIntervalCombinations should be ordered by the number of expansions used }
/// <summary> Determines whether this combination of fillings can fill the entire length, where some fillings may be expanded. </summary> /// <returns> The specified combination with the gaps; or null if the entire length couldn't be filled (even with expansions). </returns> private static CombinationWithGaps <T> CanCombinationFill <T>(IReadOnlyList <T> combination, IEnumerable <IReadOnlyList <bool> > combinationFilling, IReadOnlyList <bool> initialFilling, Interval toFill, CanExpandFillingDelegate <T> canExpand) where T : class, IFillable { //an interval is filled if all positions up to length occur once in the combinationFilling and no positions after length occur. //if there are gaps, positions next to the gaps may be queried whether they are capable of filling the gaps. A gap may be multiple positions wide. //a gap may not be filled by two partial expansions //these restricions are imposed for simplicity: perhaps later in general any gap may be filled by any position, but that doesn't seem required for now Contract.Requires(combination != null); Contract.RequiresForAll(combination, selection => toFill.Contains(new Interval(selection.Occupation)), "combination selected outside of interval to fill"); Contract.Requires(!toFill.IsEmpty); Contract.Requires(toFill.StartInclusive); Contract.Requires(!toFill.EndInclusive); int length = combination[0].Occupation.Count; Func <int, T> getOptionThatFilledIndex = positionIndex => { int i = combinationFilling.IndexOf(filling => positionIndex >= filling.Count ? false : filling[positionIndex]); if (i == -1) { //in case the leaf at the specified position index was flagged 'alreadyBound' Contract.Assert(initialFilling[positionIndex]); return(null); } return(combination[i]); }; SortedList <int> filledPositionIndices = combinationFilling.ConcatIfNotNull(initialFilling) .SelectManySorted(elementFilling => elementFilling.IndicesOf(_ => _)) .ToSortedList(); //TODO: more lazily if (containsDoubles(filledPositionIndices)) { return(null); } List <Gap> gaps = findGaps(filledPositionIndices, toFill).ToList(); foreach (Gap gap in gaps) { T substitute = null; //check whether the element to the left can fill the gap if (gap.Start != 0) { var optionThatFilled = getOptionThatFilledIndex(gap.Start - 1); if (optionThatFilled != null && canExpand(optionThatFilled, gap)) { continue; } } //check whether the element to the right can fill the gap if (gap.End < length) { Contract.Assert(substitute == null, "gap is being filled from both sides"); //just place 'else' before 'if' above: but realize that this then makes filling from the left preferable var optionThatFilled = getOptionThatFilledIndex(gap.End); if (optionThatFilled != null && canExpand(optionThatFilled, gap)) { continue; } } //some gap could not be filled return(null); } //all gaps were filled return(new CombinationWithGaps <T>(combination, gaps)); }
/// <summary> Gets all combinations of options that fill the specified length, allowing some options to expand their occupation. /// The resulting selected combinations are ordered by cumulative expansion length. </summary> /// <param name="options"> The option to fill the interval with. Each option holds the information of which spots it fills. </param> /// <param name="length"> The number of spots to fill. </param> /// <param name="initialFilling"> Specifies which indices are pre-filled. Specify null to indicate no pre-filling. </param> public static IEnumerable <CombinationWithGaps <T> > AllFillingIntervalCombinations <T>(this IReadOnlyList <T> options, int length, CanExpandFillingDelegate <T> canExpand, IReadOnlyList <bool> initialFilling = null) where T : class, IFillable { Contract.Requires(options != null); Contract.Requires(0 <= length); Contract.Requires(canExpand != null); int elementsYieldedCount_DEBUG = 0; IEnumerable <CombinationWithGaps <T> > result; result = Enumerable.Range(1, options.Count) .SelectMany(i => AllCombinations(options, i)) .LazilyAssertForAll(_ => elementsYieldedCount_DEBUG++) .Select(combination => CanCombinationFill(combination, combination.Select(element => element.Occupation), initialFilling, new Interval(0, length), canExpand)) .Where(NotNull) .OrderBy(_ => _); //perhaps AllCombinations could be made such that this is guaranteed to be already in order? To improve performance EnsureSingleEnumerationDEBUG(ref result); return(result); }