Алгоритм нахождения, если два набора множеств чисел изоморфны или нет (при перестановке)

Учитывая две системы, состоящие из множества наборов чисел, я хотел бы знать, являются ли они изоморфными под перестановкой.

Например {{1,2,3,4,5}, {2,4,5,6,7}, {2,3,4,6,7}} представляет собой систему из 3 наборов из 5 номеров. {{1,2,3,4,6}, {2,3,5,6,7}, {2,3,4,8,9}} - это еще одна система из 3 наборов из 5 номеров. Я хочу проверить, являются ли эти системы изоморфными.

Нет. Первая система использует числа {1,2,3,4,5,6,7}, вторая использует числа {1,2,3,4,5,6,7,8,9}.

Вот еще один пример. {{1,2,3}, {1,2,4}, {3,4,5}} и {{1,2,4}, {1,3,5}, {2,3,5} }. Эти две системы из 3 наборов из 3 чисел изоморфны.

Если я использую перестановку (5 3 1 2 4), где 1 становится 5, 2 становится 3 и т.д. Первый набор становится {5,3,1}. Второй становится {5,3,2}. Третий становится {1,2,4}. Таким образом, преобразованная система этой перестановкой является {{5,3,1}, {5,3,2}, {1,2,4}}, которая эквивалентно переписана на {{1,2,4}, {1, 3,5}, {2,3,5}}, поскольку меня не интересует порядок. Это вторая система, поэтому да.

В настоящее время, в первом примере, я применяю все 9! перестановки {1,2,3,..., 9} к первой системе и проверить, могу ли я получить вторую. Это дает мне ответ, но очень медленно.

Есть ли умный алгоритм?

(Мне нужен только ответ, да или нет. Мне не интересно получить перестановку, которая преобразует первую систему во вторую).

Ответ 1

Как отмечалось в комментариях, это может соответствовать теоретико-графическим проблемам, которые все еще изучаются в отношении сложности и алгоритмов, которые могут быть использованы для их решения.

Однако сложность всегда относится к некоторому размеру ввода. И здесь, неясно, каков ваш размер ввода. В качестве примера: я думаю, что наиболее подходящий алгоритм может зависеть от того, собираетесь ли вы масштабироваться...

  • количество чисел (1... 9 в вашем примере) или
  • количество наборов в каждом наборе (3 в вашем примере) или
  • размер наборов в наборах (5, в вашем примере)

Используя ваш текущий подход, масштабирование числа чисел было бы невозможным, потому что вы не можете вычислить все перестановки для чисел, намного превышающих 9 из-за экспоненциального времени работы. Но если бы ваше намерение состояло в проверке изоморфности множеств, содержащих 1000 множеств, алгоритм, который был многочлен числа наборов (если бы такой алгоритм существовал), по-прежнему может быть медленнее на практике.


Здесь я хотел бы набросать подход, который я пробовал. Я не выполнил подробный анализ сложности (что может быть бессмысленным, если вообще не существует решения полиномиального времени - и доказать или опровергнуть, что не может быть предметом ответа здесь).

Основная идея заключается в следующем:

Сначала вы вычисляете действительные "домены" для каждого номера входа. Это возможные значения, на которые можно сопоставить каждый номер на основе перестановки. Если заданные числа равны 1,2 и 3, то домены изначально могли бы быть

1 -> { 1, 2, 3 }
2 -> { 1, 2, 3 }
3 -> { 1, 2, 3 }

Но для данных множеств уже можно получить некоторую информацию, которая позволяет уменьшить домены. Например: любое число, которое появляется n раз в первых наборах, должно быть сопоставлено с числом, которое появляется n раз во вторых наборах.

Предположим, что заданные множества

{{1,2},{1,3}}
{{3,1},{3,2}}

Тогда домены будут только

1 -> { 3 }
2 -> { 1, 2 }
3 -> { 1, 2 }

потому что 1 появляется дважды в первых наборах, и единственное значение, которое появляется дважды во вторых наборах, это 3.

После вычисления начальных доменов можно выполнить откат возможных назначений (перестановок) чисел. Откат можно грубо сделать как

for (each number n that has no permutation value assigned) {
    assign a permutation value (from the current domain of n) to n
    update the domains of all other numbers
    if the domains are no longer valid, then backtrack
    if the solution was found, then return it
}

(Идея каким-то образом "вдохновлена" Arc Consistency 3 Algorithm, хотя технически проблемы напрямую не связаны)

Во время обратного отсчета можно использовать различные критерии отсечения. То есть можно подумать о различных трюках, чтобы быстро проверить, являются ли определенные назначения (частичная перестановка) и домены, которые подразумеваются этим назначением, "действительными" или нет.

Очевидным (необходимым) критерием правильности присвоения является то, что ни одна из доменов не может быть пуста. В более общем плане: каждый домен может не отображаться чаще, чем количество содержащихся в нем элементов. Когда вы узнаете, что домены

1 -> { 4 }
2 -> { 2,3 }
3 -> { 2,3 }
4 -> { 2,3 }

тогда уже не может быть корректного решения, и алгоритм может отследить назад.


Конечно, bactracking имеет тенденцию иметь экспоненциальную сложность во входном размере. Но, возможно, просто нет эффективного алгоритма для этой проблемы. В этом случае обрезка, которая может использоваться во время обратного отсчета, может по меньшей мере помочь сократить время работы для определенных случаев (или для небольших размеров ввода в целом) по сравнению с обычным поиском грубой силы.

Вот реализация моих экспериментов на Java. Это не особенно элегантно, но показывает, что он в основном работает: он быстро находит решение, если он существует, и (для заданных размеров ввода) не требуется много времени, чтобы обнаружить, когда нет решения.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class SetSetIsomorphisms
{
    public static void main(String[] args)
    {
        Map<Integer, Integer> p = new LinkedHashMap<Integer, Integer>();
        p.put(0, 3);
        p.put(1, 4);
        p.put(2, 8);
        p.put(3, 2);
        p.put(4, 1);
        p.put(5, 5);
        p.put(6, 0);
        p.put(7, 9);
        p.put(8, 7);
        p.put(9, 6);

        Set<Set<Integer>> sets0 = new LinkedHashSet<Set<Integer>>();
        sets0.add(new LinkedHashSet<Integer>(Arrays.asList(1,2,3,4,5)));
        sets0.add(new LinkedHashSet<Integer>(Arrays.asList(2,4,5,6,7)));
        sets0.add(new LinkedHashSet<Integer>(Arrays.asList(0,8,3,9,7)));

        Set<Set<Integer>> sets1 = new LinkedHashSet<Set<Integer>>();
        for (Set<Integer> set0 : sets0)
        {
            sets1.add(applyMapping(set0, p));
        }

        // Uncomment these lines for a case where NO permutation is found
        //sets1.remove(sets1.iterator().next());
        //sets1.add(new LinkedHashSet<Integer>(Arrays.asList(4,8,2,3,5)));


        System.out.println("Initially valid? "+
            areIsomorphic(sets0, sets1, p));


        boolean areIsomorphic = areIsomorphic(sets0, sets1);
        System.out.println("Result: "+areIsomorphic);
    }

    private static <T> boolean areIsomorphic(
        Set<Set<T>> sets0, Set<Set<T>> sets1)
    {
        System.out.println("sets0:");
        for (Set<T> set0 : sets0)
        {
            System.out.println("    "+set0);
        }
        System.out.println("sets1:");
        for (Set<T> set1 : sets1)
        {
            System.out.println("    "+set1);
        }

        Set<T> all0 = flatten(sets0);
        Set<T> all1 = flatten(sets1);

        System.out.println("All elements");
        System.out.println("    "+all0);
        System.out.println("    "+all1);

        if (all0.size() != all1.size())
        {
            System.out.println("Different number of elements");
            return false;
        }

        Map<T, Set<T>> domains = computeInitialDomains(sets0, sets1);

        System.out.println("Domains initially:");
        print(domains, "");

        Map<T, T> assignment = new LinkedHashMap<T, T>();
        return compute(assignment, domains, sets0, sets1, "");
    }

    private static <T> Map<T, Set<T>> computeInitialDomains(
        Set<Set<T>> sets0, Set<Set<T>> sets1)
    {
        Set<T> all0 = flatten(sets0);
        Set<T> all1 = flatten(sets1);
        Map<T, Set<T>> domains = new LinkedHashMap<T, Set<T>>();
        for (T e0 : all0)
        {
            Set<T> domain0 = new LinkedHashSet<T>();
            for (T e1 : all1)
            {
                if (isFeasible(e0, sets0, e1, sets1))
                {
                    domain0.add(e1);
                }
            }
            domains.put(e0, domain0);
        }
        return domains; 
    }

    private static <T> boolean isFeasible(
        T e0, Set<Set<T>> sets0,
        T e1, Set<Set<T>> sets1)
    {
        int c0 = countContaining(sets0, e0);
        int c1 = countContaining(sets1, e1);
        return c0 == c1;
    }

    private static <T> int countContaining(Set<Set<T>> sets, T value)
    {
        int count = 0;
        for (Set<T> set : sets)
        {
            if (set.contains(value))
            {
                count++;
            }
        }
        return count;
    }


    private static <T> boolean compute(
        Map<T, T> assignment, Map<T, Set<T>> domains, 
        Set<Set<T>> sets0, Set<Set<T>> sets1, String indent)
    {
        if (!validCounts(domains.values()))
        {
            System.out.println(indent+"There are too many domains "
                + "with too few elements");
            print(domains, indent);
            return false;
        }
        if (assignment.keySet().equals(domains.keySet()))
        {
            System.out.println(indent+"Found assignment: "+assignment);
            return true;
        }

        List<Entry<T, Set<T>>> entryList = 
            new ArrayList<Map.Entry<T,Set<T>>>(domains.entrySet());
        Collections.sort(entryList, new Comparator<Map.Entry<T,Set<T>>>()
        {
            @Override
            public int compare(Entry<T, Set<T>> e0, Entry<T, Set<T>> e1)
            {
                return Integer.compare(
                    e0.getValue().size(), 
                    e1.getValue().size());
            }
        });
        for (Entry<T, Set<T>> entry : entryList)
        {
            T key = entry.getKey();
            if (assignment.containsKey(key))
            {
                continue;
            }
            Set<T> domain = entry.getValue();
            for (T value : domain)
            {
                Map<T, Set<T>> newDomains = copy(domains);
                removeFromOthers(newDomains, key, value);
                assignment.put(key, value);
                newDomains.get(key).clear();
                newDomains.get(key).add(value);

                System.out.println(indent+"Using "+assignment);

                Set<Set<T>> setsContainingKey = 
                    computeSetsContainingValue(sets0, key);
                Set<Set<T>> setsContainingValue = 
                    computeSetsContainingValue(sets1, value);
                Set<T> keyElements = flatten(setsContainingKey);
                Set<T> valueElements = flatten(setsContainingValue);

                for (T otherKey : keyElements)
                {
                    Set<T> otherValues = newDomains.get(otherKey);
                    otherValues.retainAll(valueElements);
                }

                System.out.println(indent+"Domains when "+assignment);
                print(newDomains, indent);

                boolean done = compute(assignment, newDomains, 
                    sets0, sets1, indent+"  ");
                if (done)
                {
                    return true;
                }
                assignment.remove(key);
            }
        }
        return false;
    }


    private static boolean validCounts(
        Collection<? extends Collection<?>> collections)
    {
        Map<Collection<?>, Integer> counts = 
            new LinkedHashMap<Collection<?>, Integer>();
        for (Collection<?> c : collections)
        {
            Integer count = counts.get(c);
            if (count == null)
            {
                count = 0;
            }
            counts.put(c, count+1);
        }
        for (Entry<Collection<?>, Integer> entry : counts.entrySet())
        {
            Collection<?> c = entry.getKey();
            Integer count = entry.getValue();
            if (count > c.size())
            {
                return false;
            }
        }
        return true;
    }


    private static <K, V> Map<K, Set<V>> copy(Map<K, Set<V>> map)
    {
        Map<K, Set<V>> copy = new LinkedHashMap<K, Set<V>>();
        for (Entry<K, Set<V>> entry : map.entrySet())
        {
            K k = entry.getKey();
            Set<V> values = entry.getValue();
            copy.put(k, new LinkedHashSet<V>(values));
        }
        return copy;
    }

    private static <T> Set<Set<T>> computeSetsContainingValue(
        Iterable<? extends Set<T>> sets, T value)
    {
        Set<Set<T>> containing = new LinkedHashSet<Set<T>>();
        for (Set<T> set : sets)
        {
            if (set.contains(value))
            {
                containing.add(set);
            }
        }
        return containing;
    }

    private static <T> void removeFromOthers(
        Map<T, Set<T>> map, T key, T value)
    {
        for (Entry<T, Set<T>> entry : map.entrySet())
        {
            if (!entry.getKey().equals(key))
            {
                Set<T> values = entry.getValue();
                values.remove(value);
            }
        }
    }

    private static <T> Set<T> flatten(
        Iterable<? extends Collection<? extends T>> collections)
    {
        Set<T> set = new LinkedHashSet<T>();
        for (Collection<? extends T> c : collections)
        {
            set.addAll(c);
        }
        return set;
    }




    private static <T> Set<T> applyMapping(
        Set<T> set, Map<T, T> map)
    {
        Set<T> result = new LinkedHashSet<T>();
        for (T e : set)
        {
            result.add(map.get(e));
        }
        return result;
    }

    private static <T> boolean areIsomorphic(
        Set<Set<T>> sets0, Set<Set<T>> sets1, Map<T, T> p)
    {
        for (Set<T> set0 : sets0)
        {
            Set<T> set1 = applyMapping(set0, p);
            if (!sets1.contains(set1))
            {
                return false;
            }
        }
        return true;
    }

    private static void print(Map<?, ?> map, String indent)
    {
        for (Entry<?, ?> entry : map.entrySet())
        {
            System.out.println(indent+entry.getKey()+": "+entry.getValue());
        }
    }

}

Ответ 2

Я считаю, что ваша проблема эквивалентна проблеме изоморфизма графа (GI). Ваш набор наборов может быть смоделирован как (двудольный) граф с узлами, представляющими базовые значения вашего набора (например, 1, 2, 3,... 7), а узлы справа представляют собой множества (например, {1, 2,3,4,6} или {2,3,5,6,7}). Нарисуйте ребро, соединяющее node слева с node справа, если число является элементом набора; в моем примере 1 подключен только к {1,2,3,4,6}, а 2 подключен как к {1,2,3,4,6}, так и к {2,3,5,6,7}, 1 подключен ко всем наборам, которые его содержат; {1,2,3,4,6} связано со всеми содержащимися в нем числами.

Любой двудольный граф может быть реализован таким образом. Напротив, GI можно свести к решению GI на двудольных графах. (Любой граф можно преобразовать в двудольный граф, заменив каждое ребро двумя новыми ребрами и новой вершиной. Изоморфизм в полученных двудольных графах эквивалентен изоморфизму в исходных графах.)

GI находится в NP, но неизвестно, является ли он полным. На практике GI можно быстро решить для сотен вершин, например, NAUTY.