简体   繁体   中英

Loop - control flow for working with 2 lists of unknown length

Suppose 2 list of strings of unknown, possibly different length. Items in the second list must be matched to the items in the first list, but the matching may depend on the content of the following items:

List lst_1: [0] Apple [1] Orange [2] Peach [3] ...

List lst_2: [0] salt [1] tequila [2] honey [3] vodka [4] whisky [5] ...

Items in lst_1 must be matched with items in lst_2 based on several conditions. For instance, for each fruit, there must be at least one alcohol; whiskey can not be matched with peach; there must be less than 5 alcohols in any given cocktail, etc.

If I match Apple with salt and tequila, orange with honey and vodka, I get peach with whisky, which breaks one of the conditions... but if I put apple, tequila and honey together, I get Orange-Vodka-Whiskey, which is a valid drink.

Bottom line, I need to loop through both lists, each time checking a number of conditions for the current item as well as what it means for the next item on the list and for the previous item, and I might need to back track several times and fix things until everything matches properly.

I was thinking of a big while(bNotReady){...} loop like this:

int i = 0;
int j = 0;
string fruit, additive;
bool bContainsAlcohol = false;
dictionary<string, string> dic = new dictionary<string,string>();
while(bNotReady){
  fruit = lst_1[i];
  additive = lst_2[j];
  if (is_valid_match(fruit,additive) && bContainsAlcohol)
  {
     dic.Add(fruit,additive);
     i++; j++;
     continue;
  }
  else if(...)

}

And working my way through the lists, but I can see this would quickly become one huge, unreadable loop.

Is there a better way to work out the control flow for this task?

If you decide to go for the constraint programming path i think that it would be a better idea, yet you can do it with brute force and .Net collections. It won't be pretty but it should work.

  • First, you need a way to enumerate all the possible partitions of your additives into sets with a cardinality equal to the number of ingredients. [a,b,c] partitioned into three sets could give [abc,,] then [ab,c,] then [a,bc,] , etc..
  • Enumerating all these sets you would need to enumerate all the permutations of these sets
  • Finally, for each permutation of a set you would need to check whether all the rules are satisfied by the current match between ingredient and set of additives

I didn't have time to create a method for the partitioning but i had a permutation extension available so i created the second step of the program

private static IList<string> Ingredients { get; set; }
private static IList<string> Additives { get; set; }
private static IList<Func<string, string, bool>> Rules { get; set; }

private static void Main(string[] args)
{
    Ingredients = new List<string>() { "Apple", "Orange", "Peach" };
    Additives = new List<string>() { "Vodka", "Rum", "Whiskey" };
    Rules = new List<Func<string, string, bool>>() { (ingredient1, ingredient2) => { return (ingredient1 != "Peach" && ingredient2 != "Whiskey"); } };
    var additivesOrganisationMatchingAllTheRules = FindMatch();
}

private static IList<string> FindMatch()
{
    // here we should enumerate all sets and then enumerate permutation of all the sets
    // instead for the example we just enumerate the permutations
    foreach (var additivesPermutation in Additives.GetCombinations())
    {
        for (int i = 0; i < additivesPermutation.Count; i++)
        {
            var thisSituationIsOk = Rules.All(r => r(Ingredients[i], Additives[i]));
            if (thisSituationIsOk) return additivesPermutation;
        }
    }
    return null;
}

It uses a permutation method extension; from what i can remember this extension doesn't preserve the initial list. Don't use without tests

public static class CombinatorialExtension
{
    public static IEnumerable<IList<TSource>> GetCombinations<TSource>(
        this IList<TSource> source)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        return GetCombinationsImpl<TSource>(source);
    }

    private static IEnumerable<IList<TSource>> GetCombinationsImpl<TSource>(
        this IList<TSource> list)
    {
        return Permutations(list, list.Count);
    }

    private static void ShiftRight<TSource>(IList<TSource> list, int cardinality)
    {
        var lastElement = list[cardinality - 1];
        list.RemoveAt(cardinality - 1);
        list.Insert(0, lastElement);
    }

    private static IEnumerable<IList<TSource>> Permutations<TSource>(IList<TSource> list, int cardinality)
    {
        if (cardinality == 1)
        {
            yield return list;
        }
        else
        {
            for (int i = 0; i < cardinality; i++)
            {
                foreach (var perm in Permutations(list, cardinality - 1))
                    yield return perm;
                ShiftRight(list, cardinality);
            }
        }
    }
}

This algorithm would let you find a solution that matches the ingredients and rules you have, but it is not very pretty, and really really not efficient: many combinations would be computed multiple times. You would also have to tweak the rules to get results that would be pleasant (ie minimum and maximum number of additives, etc)


EDIT

It is even possible to avoid having to create all the sets, you can simply add as many separators into your additives as you have ingredients minus 1 . Then just do the permutation and split your additives according to the separators into lists of additives. Then your rules can take an ingredient and a list of additives as a basis to check if it is respected. Here is some sample code

private static IList<string> Ingredients { get; set; }
private static IList<string> Additives { get; set; }
private static IList<Func<string, IList<string>, bool>> Rules { get; set; }

private static void Main(string[] args)
{
    Ingredients = new List<string>() { "Apple", "Orange", "Peach" };
    Additives = new List<string>() { "Vodka", "Rum", "Whiskey" };
    Additives.Add("Separator");
    Additives.Add("Separator"); // add as many separators as the number of ingredients - 1
    Rules = new List<Func<string, IList<string>, bool>>() {
        (ingredient1, ingredient2) => { return (ingredient1 != "Peach" && ingredient2.All(s => s != "Whiskey")); }
        , 
        (ingredient1, ingredient2) => { return ingredient2.Count > 0; }
    };
    var additivesOrganisationMatchingAllTheRules = FindMatch();
}

private static IList<IList<string>> FindMatch()
{
    // separators will create the sets
    foreach (var additivesPermutation in Additives.GetCombinations())
    {
        var Sets = Split(additivesPermutation);
        var thisSituationIsOk = true;
        for (int i = 0; i < Sets.Count && thisSituationIsOk ; i++)
        {
            thisSituationIsOk = thisSituationIsOk && Rules.All(r => r(Ingredients[i], Sets[i]));
        }
        if (thisSituationIsOk) return Sets;
    }
    return null;
}

private static IList<IList<string>> Split(IList<string> values)
{
    var splitValues = new List<IList<String>>();
    var currentList = new List<string>();
    foreach (var value in values)
    {
        if (value == "Separator")
        {
            splitValues.Add(currentList);
            currentList = new List<string>();
        }
        else
        {
            currentList.Add(value);
        }
    }
    splitValues.Add(currentList);
    return splitValues;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM