寻找两组数字集合在置换下是否同构的算法

11
给定两个由一组数字集合组成的系统,我想知道它们是否在置换下同构。
例如{{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}}。这两个由三个包含三个数字的集合组成的系统是同构的。
如果我使用置换(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}},因为我不关心顺序。这是第二个系统,所以答案是是的。
目前,在第一个例子中,我将{1,2,3,...,9}的所有9!个置换应用于第一个系统,并检查是否可以得到第二个系统。它会给出一个答案,但非常慢。
有没有聪明的算法?
(我只想要答案,即是或否。我对获取将第一个系统转换为第二个系统的置换不感兴趣。)

不太明白你的问题,集合是无序的。你是指“这两个系统是否包含相同的对象集合”吗? - G. Bach
我的答案不需要顺序,所以我使用了集合。这是我进行转换的方式:对于每个排列p,我将每个数字i替换为p[i]得到变换后的系统。然后,我比较变换后的集合和第二个集合,看它们是否包含相同的数字集合。 - montardon
我还是不明白。你想要将什么映射到一起,你有什么同构的想法?任何具有相等基数的两个集合在适当的代数意义下都是同构的。此外,如果你谈论置换,顺序很重要。 - G. Bach
2
我想要一些关于你所谈论的同构类型的澄清。在将3和4交换的置换下,{{1, 2, 3}}和{{1, 2, 4}}是否同构? - user2357112
4
这是超图同构问题,它是图同构问题的推广。虽然我不确定解决这两个问题的现有技术水平,但我知道图同构问题仍然被认为是极其困难的,尚不清楚是否存在多项式时间算法来解决它。 - user2357112
显示剩余7条评论
2个回答

5
正如评论中指出的那样,这可能对应于图论问题,这些问题仍在研究其复杂性和可用于解决它们的算法方面。然而,复杂度总是指一些输入大小。在这里,你的输入大小并不清楚。例如:我认为最合适的算法可能取决于你是否要扩展...
- 数字的数量(在你的例子中为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 }

那么就不再存在有效的解决方案,算法可能会回溯。


当然,回溯算法在输入规模上往往具有指数级的复杂度。但是可能根本不存在有效的算法来解决这个问题。对于这种情况,回溯期间可能采用的修剪至少可以帮助减少某些情况下(或者一般情况下的小输入规模)的运行时间,与暴力穷举搜索相比。
这是我在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());
        }
    }

}

对于我的问题,我只改变了集合大小和数字大小。每个集合的大小是恒定的。 - montardon
感谢您的回答和Java实现。 - montardon
@tqbfjotld 你不想等待接受答案吗?也许稍后会有其他人发布更好的想法...? - Marco13
好的,我会稍等一下。 - montardon
@PhilippeMorin 现在那可是有点儿大的“位”数了;-) 谢谢,希望它能解决你的问题。 - Marco13

2
我相信你的问题等同于图同构问题(GI)。你的一组集合可以建模为一个(二分)图,左侧节点表示集合的基础值(例如1、2、3、…7),右侧节点表示集合(例如{1,2,3,4,6}或{2,3,5,6,7})。如果数字是集合的元素,则在左侧节点和右侧节点之间绘制边缘;在我的示例中,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的难度。(通过使用两个新边缘和一个新顶点替换每条边缘,可以将任何图形转换为二分图。结果二分图中的同构等同于原始图形中的同构。)
GI属于NP问题,但尚不清楚它是否属于NP完全问题。在实践中,例如NAUTY可以快速解决数百个顶点的GI问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接