Trouvez le plus grand ensemble indépendant dans un graphique en treillis de grande dimension


16

Pour un entier positif donné n, considérez toutes les chaînes binaires de longueur 2n-1. Pour une chaîne donnée S, Lsoit un tableau de longueur nqui contient le compte du nombre de 1s dans chaque sous-chaîne de longueur nde S. Par exemple, si n=3et S = 01010alors L=[1,2,1]. Nous appelons Lle tableau de comptage de S.

On dit que deux chaînes S1et S2de la même longueur correspondance si leurs tableaux de comptage respectifs L1et L2ont la propriété L1[i] <= 2*L2[i]et L2[i] <= 2*L1[i]pour tous i.

Tâche

Pour augmenter à npartir de n=1, la tâche consiste à trouver la taille du plus grand ensemble de chaînes, chacune de longueur 2n-1afin qu'aucune chaîne ne corresponde.

Votre code doit afficher un nombre par valeur de n.

But

Votre score est le plus élevé npour lequel personne d'autre n'a publié de réponse correcte plus élevée pour aucune de vos réponses. De toute évidence, si vous avez toutes les réponses optimales, vous obtiendrez le score le plus élevé que nvous publiez. Cependant, même si votre réponse n'est pas optimale, vous pouvez toujours obtenir le score si personne d'autre ne peut le battre.

Exemples de réponses

Car n=1,2,3,4je reçois 2,4,10,16.

Langues et bibliothèques

Vous pouvez utiliser toutes les langues et bibliothèques disponibles que vous aimez. Dans la mesure du possible, il serait bon de pouvoir exécuter votre code, veuillez donc inclure une explication complète sur la façon d'exécuter / compiler votre code sous Linux si possible.

Entrées principales

  • 5 de Martin Büttner dans Mathematica
  • 6 par Reto Koradi en C ++ . Les valeurs sont 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086. Les 5 premiers sont connus pour être optimaux.
  • 7 par Peter Taylor à Java . Les valeurs sont 2, 4, 10, 16, 31, 47, 76, 111, 166, 235.
  • 9 par joriki en Java . Les valeurs sont 2, 4, 10, 16, 31, 47, 76, 112, 168.

3
Je pense qu'il est plus naturel de comprendre l'inégalité lorsqu'elle est notée comme L1[i]/2 <= L2[i] <= 2*L1[i].
orlp

1
De plus, l'appariement n'est pas une relation d'équivalence. match(A, B)et match(B, C)n'implique pas match(A, C)(idem pour l'inverse). Exemple: [1] et [2] correspondent, [2] et [3] correspondent, mais [1] et [3] ne correspondent pas. De même, [1,3] et [3,1] ne correspondent pas, [3, 1] et [2, 3] ne correspondent pas, mais [1, 3] et [2, 3] correspondent.
orlp

Réponses:


7

2, 4, 10, 16, 31, 47, 76, 112, 168

Pour chaque n, ce code Java détermine les tableaux de comptage possibles et trouve ensuite des ensembles non correspondants de taille croissante, pour chaque taille commençant par un ensemble aléatoire et l'améliorant par descente la plus raide randomisée. À chaque étape, l'un des éléments de l'ensemble est sélectionné de manière aléatoire uniforme et remplacé par un autre tableau de comptage sélectionné de manière aléatoire uniforme parmi ceux qui ne sont pas utilisés. L'étape est acceptée si elle n'augmente pas le nombre de correspondances. Cette dernière prescription semble cruciale; accepter les étapes uniquement si elles réduisent le nombre de correspondances n'est pas aussi efficace. Les étapes laissant le nombre de correspondances invariant permettent d'explorer l'espace de recherche, et éventuellement un peu d'espace peut s'ouvrir pour éviter l'une des correspondances. Après 2 ^ 24 étapes sans amélioration, la taille précédente est sortie pour la valeur actuelle de n et n est incrémenté.

Les résultats jusqu'à n = 9 sont 2, 4, 10, 16, 31, 47, 76, 112, 168, améliorant les résultats précédents pour n = 8 par un et pour n = 9 par deux. Pour des valeurs plus élevées de n, la limite de 2 ^ 24 pas peut devoir être augmentée.

J'ai également essayé un recuit simulé (avec le nombre de matchs comme énergie), mais sans amélioration par rapport à la descente la plus raide.

Code

enregistrer en tant que Question54354.java
compilation avec javac Question54354.java
exécuter avecjava Question54354

import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public class Question54354 {
    static class Array {
        int [] arr;

        public Array (int [] arr) {
            this.arr = arr;
        }

        public int hashCode () {
            return Arrays.hashCode (arr);
        }

        public boolean equals (Object o) {
            return Arrays.equals (((Array) o).arr,arr);
        }
    }

    static int [] indices;
    static int [] [] counts;
    static boolean [] used;

    static Random random = new Random (0);

    static boolean match (int [] c1,int [] c2) {
        for (int k = 0;k < c1.length;k++)
            if (c1 [k] > 2 * c2 [k] || c2 [k] > 2 * c1 [k])
                return false;
        return true;
    }

    static int matches (int i) {
        int result = 0;
        for (int j = 0;j < indices.length;j++)
            if (j != i && match (counts [indices [i]],counts [indices [j]]))
                result++;
        return result;
    }

    static void randomize (int i) {
        do
            indices [i] = random.nextInt (counts.length);
        while (used [indices [i]]);
    }

    public static void main (String [] args) {
        for (int n = 1,length = 1;;n++,length += 2) {
            int [] lookup = new int [1 << n];
            for (int string = 0;string < 1 << n;string++)
                for (int bit = 1;bit < 1 << n;bit <<= 1)
                    if ((string & bit) != 0)
                        lookup [string]++;
            Set<Array> arrays = new HashSet<Array> ();
            for (int string = 0;string < 1 << length;string++) {
                int [] count = new int [n];
                for (int i = 0;i < n;i++)
                    count [i] = lookup [(string >> i) & ((1 << n) - 1)];
                arrays.add (new Array (count));
            }
            counts = new int [arrays.size ()] [];
            int j = 0;
            for (Array array : arrays)
                counts [j++] = array.arr;
            used = new boolean [counts.length];

            int m;
            outer:
            for (m = 1;m <= counts.length;m++) {
                indices = new int [m];
                for (;;) {
                    Arrays.fill (used,false);
                    for (int i = 0;i < m;i++) {
                        randomize (i);
                        used [indices [i]] = true;
                    }
                    int matches = 0;
                    for (int i = 0;i < m;i++)
                        matches += matches (i);
                    matches /= 2;
                    int stagnation = 0;
                    while (matches != 0) {
                        int k = random.nextInt (m);
                        int oldMatches = matches (k);
                        int oldIndex = indices [k];
                        randomize (k);
                        int newMatches = matches (k);
                        if (newMatches <= oldMatches) {
                            if (newMatches < oldMatches) {
                                matches += newMatches - oldMatches;
                                stagnation = 0;
                            }
                            used [oldIndex] = false;
                            used [indices [k]] = true;
                        }
                        else
                            indices [k] = oldIndex;

                        if (++stagnation == 0x1000000)
                            break outer;
                    }
                    break;
                }
            }
            System.out.println (n + " : " + (m - 1));
        }
    }
}

1
Une très belle amélioration!

11

2, 4, 10, 16, 31, 47, 76, 111, 166, 235

Remarques

Si l' on considère un graphe Gdont les sommets sont 0à net arêtes joignant deux numéros qui correspondent, les puissance tenseur G^n a des sommets qui (x_0, ..., x_{n-1})forment le pouvoir cartésien {0, ..., n}^net les bords correspondants entre tuples. Le graphique qui nous intéresse est le sous-graphique deG^n induits par les sommets qui correspondent aux possibles "tableaux de comptage".

La première sous-tâche consiste donc à générer ces sommets. L'approche naïve énumère les 2^{2n-1}chaînes, ou sur l'ordre de 4^n. Mais si nous regardons plutôt le tableau des premières différences des tableaux de comptage, nous constatons qu'il n'y a que des 3^npossibilités, et à partir des premières différences, nous pouvons déduire la plage de valeurs initiales possibles en exigeant qu'aucun élément des différences nulles ne soit inférieur à 0ou supérieur à n.

Nous voulons ensuite trouver l'ensemble indépendant maximum. J'utilise un théorème et deux heuristiques:

  • Théorème: l'ensemble indépendant maximum d'une union disjointe de graphes est l'union de leurs ensembles indépendants maximum. Donc, si nous décomposons un graphique en composants non connectés, nous pouvons simplifier le problème.
  • Heuristique: supposons que ce (n, n, ..., n)sera dans un ensemble indépendant maximum. Il y a une assez grande clique de sommets {m, m+1, ..., n}^nmest le plus petit entier qui correspond n;(n, n, ..., n)est garanti de n'avoir aucun match en dehors de cette clique.
  • Heuristique: adoptez l'approche gourmande de sélection du sommet du plus bas degré.

Sur mon ordinateur ce trouve 111pour n=8en 16 secondes, 166pour n=9en 8 minutes environ, et 235pour n=10environ 2 heures.

Code

Enregistrez sous PPCG54354.java, compilez sous javac PPCG54354.javaet exécutez sous java PPCG54354.

import java.util.*;

public class PPCG54354 {
    public static void main(String[] args) {
        for (int n = 1; n < 20; n++) {
            long start = System.nanoTime();

            Set<Vertex> constructive = new HashSet<Vertex>();
            for (int i = 0; i < (int)Math.pow(3, n-1); i++) {
                int min = 0, max = 1, diffs[] = new int[n-1];
                for (int j = i, k = 0; k < n-1; j /= 3, k++) {
                    int delta = (j % 3) - 1;
                    if (delta == -1) min++;
                    if (delta != 1) max++;
                    diffs[k] = delta;
                }

                for (; min <= max; min++) constructive.add(new Vertex(min, diffs));
            }

            // Heuristic: favour (n, n, ..., n)
            Vertex max = new Vertex(n, new int[n-1]);
            Iterator<Vertex> it = constructive.iterator();
            while (it.hasNext()) {
                Vertex v = it.next();
                if (v.matches(max) && !v.equals(max)) it.remove();
            }

            Set<Vertex> ind = independentSet(constructive, n);
            System.out.println(ind.size() + " after " + ((System.nanoTime() - start) / 1000000000L) + " secs");
        }
    }

    private static Set<Vertex> independentSet(Set<Vertex> vertices, int dim) {
        if (vertices.size() < 2) return vertices;

        for (int idx = 0; idx < dim; idx++) {
            Set<Set<Vertex>> p = connectedComponents(vertices, idx);
            if (p.size() > 1) {
                Set<Vertex> ind = new HashSet<Vertex>();
                for (Set<Vertex> part : connectedComponents(vertices, idx)) {
                    ind.addAll(independentSet(part, dim));
                }
                return ind;
            }
        }

        // Greedy
        int minMatches = Integer.MAX_VALUE;
        Vertex minV = null;
        for (Vertex v0 : vertices) {
            int numMatches = 0;
            for (Vertex vi : vertices) if (v0.matches(vi)) numMatches++;
            if (numMatches < minMatches) {
                minMatches = numMatches;
                minV = v0;
            }
        }

        Set<Vertex> nonmatch = new HashSet<Vertex>();
        for (Vertex vi : vertices) if (!minV.matches(vi)) nonmatch.add(vi);
        Set<Vertex> ind = independentSet(nonmatch, dim);
        ind.add(minV);
        return ind;
    }

    // Separates out a set of vertices which form connected components when projected into the idx axis.
    private static Set<Set<Vertex>> connectedComponents(Set<Vertex> vertices, final int idx) {
        List<Vertex> sorted = new ArrayList<Vertex>(vertices);
        Collections.sort(sorted, new Comparator<Vertex>() {
                public int compare(Vertex a, Vertex b) {
                    return a.x[idx] - b.x[idx];
                }
            });

        Set<Set<Vertex>> connectedComponents = new HashSet<Set<Vertex>>();
        Set<Vertex> current = new HashSet<Vertex>();
        int currentVal = 0;
        for (Vertex v : sorted) {
            if (!match(currentVal, v.x[idx]) && !current.isEmpty()) {
                connectedComponents.add(current);
                current = new HashSet<Vertex>();
            }

            current.add(v);
            currentVal = v.x[idx];
        }

        if (!current.isEmpty()) connectedComponents.add(current);
        return connectedComponents;
    }

    private static boolean match(int a, int b) {
        return a <= 2 * b && b <= 2 * a;
    }

    private static class Vertex {
        final int[] x;
        private final int h;

        Vertex(int[] x) {
            this.x = x.clone();

            int _h = 0;
            for (int xi : x) _h = _h * 37 + xi;
            h = _h;
        }

        Vertex(int x0, int[] diffs) {
            x = new int[diffs.length + 1];
            x[0] = x0;
            for (int i = 0; i < diffs.length; i++) x[i+1] = x[i] + diffs[i];

            int _h = 0;
            for (int xi : x) _h = _h * 37 + xi;
            h = _h;
        }

        public boolean matches(Vertex v) {
            if (v == this) return true;
            if (x.length != v.x.length) throw new IllegalArgumentException("v");
            for (int i = 0; i < x.length; i++) {
                if (!match(x[i], v.x[i])) return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            return h;
        }

        @Override
        public boolean equals(Object obj) {
            return (obj instanceof Vertex) && equals((Vertex)obj);
        }

        public boolean equals(Vertex v) {
            if (v == this) return true;
            if (x.length != v.x.length) return false;
            for (int i = 0; i < x.length; i++) {
                if (x[i] != v.x[i]) return false;
            }
            return true;
        }

        @Override
        public String toString() {
            if (x.length == 0) return "e";

            StringBuilder sb = new StringBuilder(x.length);
            for (int xi : x) sb.append(xi < 10 ? (char)('0' + xi) : (char)('A' + xi - 10));
            return sb.toString();
        }
    }
}

10

Mathematica n = 5,, 31 cordes

Je viens d'écrire une solution de force brute en utilisant les fonctions intégrées de Mathematica pour vérifier les exemples de réponses de Lembik, mais elle peut également gérer n = 5:

n = 5;
s = Tuples[{0, 1}, 2 n - 1];
l = Total /@ Partition[#, n, 1] & /@ s
g = Graph[l, 
  Cases[Join @@ Outer[UndirectedEdge, l, l, 1], 
   a_ <-> b_ /; 
    a != b && And @@ Thread[a <= 2 b] && And @@ Thread[b <= 2 a]]]
set = First@FindIndependentVertexSet[g]
Length@set

En prime, ce code produit une visualisation du problème sous forme de graphique où chaque bord indique deux chaînes correspondantes.

Voici le graphique pour n = 3:

entrez la description de l'image ici


2
Au début, je pensais que le graphique était bien symétrique, mais j'ai ensuite vu le point légèrement décalé sur la gauche. Impossible de ne pas voir :(
orlp

3

C ++ (heuristique): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086

Ceci est légèrement derrière le résultat de Peter Taylor, étant 1 à 3 plus bas pour n=7, 9et10 . L'avantage est que c'est beaucoup plus rapide, donc je peux l'exécuter pour des valeurs plus élevées de n. Et cela peut être compris sans mathématiques sophistiquées. ;)

Le code actuel est dimensionné pour s'exécuter jusqu'à n=15. Les temps d'exécution augmentent d'environ un facteur 4 pour chaque augmentation de n. Par exemple, elle était de 0,008 seconde pour n=7, 0,07 seconde pour n=9, 1,34 seconde pour n=11, 27 secondes pour n=13et 9 minutes pourn=15 .

Il y a deux observations clés que j'ai utilisées:

  • Au lieu d'opérer sur les valeurs elles-mêmes, l'heuristique opère sur les tableaux de comptage. Pour ce faire, une liste de tous les tableaux de comptage uniques est générée en premier.
  • L'utilisation de tableaux de comptage avec de petites valeurs est plus avantageuse, car ils éliminent moins d'espace de solution. Ceci est basé sur chaque comptage en cexcluant la plage de c / 2à 2 * cd'autres valeurs. Pour les valeurs plus petites de c, cette plage est plus petite, ce qui signifie que moins de valeurs sont exclues en l'utilisant.

Générer des tableaux de comptage uniques

J'ai utilisé la force brute sur celui-ci, en parcourant toutes les valeurs, en générant le tableau de comptage pour chacune d'entre elles et en unifiant la liste résultante. Cela pourrait certainement être fait plus efficacement, mais c'est assez bon pour les types de valeurs avec lesquelles nous fonctionnons.

C'est extrêmement rapide pour les petites valeurs. Pour les valeurs plus élevées, les frais généraux deviennent substantiels. Par exemple, pour n=15, il utilise environ 75% de la durée totale d'exécution. Ce serait certainement un domaine à examiner lorsque vous essayez d'aller beaucoup plus haut que n=15. Même l'utilisation de la mémoire pour construire une liste des tableaux de comptage pour toutes les valeurs commencerait à devenir problématique.

Le nombre de tableaux de comptage uniques représente environ 6% du nombre de valeurs pour n=15. Ce nombre relatif devient plus petit à mesure qu'il ndevient plus grand.

Sélection gourmande de valeurs de tableau de comptage

La partie principale de l'algorithme sélectionne les valeurs de tableau de comptage dans la liste générée en utilisant une approche simple et gourmande.

Basé sur la théorie selon laquelle l'utilisation de tableaux de comptage avec de petits comptes est bénéfique, les tableaux de comptage sont triés par la somme de leurs comptes.

Ils sont ensuite vérifiés dans l'ordre et une valeur est sélectionnée si elle est compatible avec toutes les valeurs précédemment utilisées. Cela implique donc un seul passage linéaire à travers les tableaux de comptage uniques, où chaque candidat est comparé aux valeurs précédemment sélectionnées.

J'ai quelques idées sur la façon dont l'heuristique pourrait potentiellement être améliorée. Mais cela semblait être un point de départ raisonnable et les résultats semblaient plutôt bons.

Code

Ce n'est pas très optimisé. J'avais une structure de données plus élaborée à un moment donné, mais il aurait fallu plus de travail pour la généraliser au n=8- delà , et la différence de performance ne semblait pas très importante.

#include <cstdint>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <sstream>
#include <iostream>

typedef uint32_t Value;

class Counter {
public:
    static void setN(int n);

    Counter();
    Counter(Value val);

    bool operator==(const Counter& rhs) const;
    bool operator<(const Counter& rhs) const;

    bool collides(const Counter& other) const;

private:
    static const int FIELD_BITS = 4;
    static const uint64_t FIELD_MASK = 0x0f;

    static int m_n;
    static Value m_valMask;

    uint64_t fieldSum() const;

    uint64_t m_fields;
};

void Counter::setN(int n) {
    m_n = n;
    m_valMask = (static_cast<Value>(1) << n) - 1;
}

Counter::Counter()
  : m_fields(0) {
}

Counter::Counter(Value val) {
    m_fields = 0;
    for (int k = 0; k < m_n; ++k) {
        m_fields <<= FIELD_BITS;
        m_fields |= __builtin_popcount(val & m_valMask);
        val >>= 1;
    }
}

bool Counter::operator==(const Counter& rhs) const {
    return m_fields == rhs.m_fields;
}

bool Counter::operator<(const Counter& rhs) const {
    uint64_t lhsSum = fieldSum();
    uint64_t rhsSum = rhs.fieldSum();
    if (lhsSum < rhsSum) {
        return true;
    }
    if (lhsSum > rhsSum) {
        return false;
    }

    return m_fields < rhs.m_fields;
}

bool Counter::collides(const Counter& other) const {
    uint64_t fields1 = m_fields;
    uint64_t fields2 = other.m_fields;

    for (int k = 0; k < m_n; ++k) {
        uint64_t c1 = fields1 & FIELD_MASK;
        uint64_t c2 = fields2 & FIELD_MASK;

        if (c1 > 2 * c2 || c2 > 2 * c1) {
            return false;
        }

        fields1 >>= FIELD_BITS;
        fields2 >>= FIELD_BITS;
    }

    return true;
}

int Counter::m_n = 0;
Value Counter::m_valMask = 0;

uint64_t Counter::fieldSum() const {
    uint64_t fields = m_fields;
    uint64_t sum = 0;
    for (int k = 0; k < m_n; ++k) {
        sum += fields & FIELD_MASK;
        fields >>= FIELD_BITS;
    }

    return sum;
}

typedef std::vector<Counter> Counters;

int main(int argc, char* argv[]) {
    int n = 0;
    std::istringstream strm(argv[1]);
    strm >> n;

    Counter::setN(n);

    int nBit = 2 * n - 1;
    Value maxVal = static_cast<Value>(1) << nBit;

    Counters allCounters;

    for (Value val = 0; val < maxVal; ++val) {
        Counter counter(val);
        allCounters.push_back(counter);
    }

    std::sort(allCounters.begin(), allCounters.end());

    Counters::iterator uniqEnd =
        std::unique(allCounters.begin(), allCounters.end());
    allCounters.resize(std::distance(allCounters.begin(), uniqEnd));

    Counters solCounters;
    int nSol = 0;

    for (Value idx = 0; idx < allCounters.size(); ++idx) {
        const Counter& counter = allCounters[idx];

        bool valid = true;
        for (int iSol = 0; iSol < nSol; ++iSol) {
            if (solCounters[iSol].collides(counter)) {
                valid = false;
                break;
            }
        }

        if (valid) {
            solCounters.push_back(counter);
            ++nSol;
        }
    }

    std::cout << "result: " << nSol << std::endl;

    return 0;
}

J'avais des solutions récursives basées sur un code similaire qui sont garanties de trouver le maximum. Mais cela a n=4déjà pris du temps. Cela aurait pu se terminer n=5avec un peu de patience. Vous devez avoir utilisé une meilleure stratégie de retour arrière pour y arriver n=7. La vôtre était-elle heuristique ou a-t-elle exploré tout l'espace de la solution? J'envisage quelques idées sur la façon d'améliorer cela, soit en affinant l'ordre de tri, soit en n'étant peut-être pas purement gourmand.
Reto Koradi

Je crois comprendre qu'il n'y a pas de retour en arrière dans la réponse de Peter Taylor. C'est purement gourmand. Les principales astuces sont qu'il réduit le nombre de tableaux de comptage à 3^net les deux heuristiques qu'il décrit.

@Lembik Mon commentaire était en réponse à un commentaire qui a été supprimé. Le nombre de tableaux de comptage devrait être le même, car je le construisais en fonction des valeurs réelles et le réduisais uniquement à celles uniques. J'ai mis à jour une version de retour en arrière de l'algorithme. Même s'il ne se termine pas dans un délai raisonnable, il trouve 76 pour n=7rapidement. Mais pour l'essayer n=9, il était toujours bloqué à 164 lorsque je l'ai arrêté après 20 minutes. Donc, étendre cela avec une forme limitée de retour en arrière simple ne semble pas généralement prometteur.
Reto Koradi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.