Знайдіть найбільший незалежний набір у високомірному гратчастому графіку


16

Для заданого натурального числа nрозглянемо всі двійкові рядки довжини 2n-1. Для цього рядка S, НЕ кажучи Lбути масивом довжиною , nякий містить лічильник числа 1х в кожній підрядку довжиною nз S. Наприклад, якщо n=3і S = 01010тоді L=[1,2,1]. Ми називаємо Lлічильний масив S.

Ми говоримо, що два рядки S1і S2однакової довжини збігаються, якщо їх відповідні масиви підрахунку L1і L2мають властивість, що L1[i] <= 2*L2[i]і L2[i] <= 2*L1[i]для всіх i.

Завдання

Для збільшення, nпочинаючи з початку n=1, завдання полягає в тому, щоб знайти розмір найбільшого набору рядків, кожна довжиною 2n-1так, щоб не збігалися дві струни.

Ваш код повинен виводити одне число на значення n.

Оцінка

Ваш бал є найвищим, nза який ніхто не опублікував більш правильну відповідь на будь-яку з ваших відповідей. Зрозуміло, що якщо у вас є всі оптимальні відповіді, ви отримаєте бал за найвищий рівеньn посаду. Однак, навіть якщо ваша відповідь не є оптимальною, ви все одно можете отримати бал, якщо ніхто більше не може його перемогти.

Приклад відповідей

Для n=1,2,3,4 я отримую 2,4,10,16.

Мови та бібліотеки

Ви можете використовувати будь-яку доступну мову та бібліотеки, які вам подобаються. Там, де це можливо, було б добре запустити свій код, тому, будь-ласка, включіть повне пояснення, як запустити / скласти свій код в Linux, якщо це можливо.

Провідні записи

  • 5 Мартін Бюттнер з Математики
  • 6 Рето Кораді в C ++ . Цінності є2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086 . Перші 5, як відомо, є оптимальними.
  • 7 Пітер Тейлор на Яві . Цінності є 2, 4, 10, 16, 31, 47, 76, 111, 166, 235.
  • 9 від joriki на Java . Цінності є 2, 4, 10, 16, 31, 47, 76, 112, 168.

3
Я думаю, що більш природним є розуміння нерівності, коли це позначено як L1[i]/2 <= L2[i] <= 2*L1[i].
orlp

1
Крім того, відповідність не є відношенням еквівалентності. match(A, B)і match(B, C)не означає match(A, C)(те ж саме для зворотного). Приклад: [1] і [2] збігаються, [2] і [3] відповідають, але [1] і [3] не відповідають. Так само [1,3] та [3,1] не відповідають, [3, 1] і [2, 3] не відповідають, але [1, 3] і [2, 3] збігаються.
orlp

Відповіді:


7

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

Для кожного n цей код Java визначає можливі масиви підрахунку, а потім знаходить невідповідні набори збільшення розміру, для кожного розміру починаючи з випадкового набору та покращуючи його шляхом рандомізованого найкрутішого спуску. На кожному кроці один з елементів набору випадково рівномірно вибирається і замінюється іншим підрахунковим масивом, довільно рівномірно вибраним серед тих, що не використовуються. Крок приймається, якщо він не збільшує кількість збігів. Цей останній рецепт видається вирішальним; прийняття кроків, лише якщо вони зменшують кількість збігів, не є настільки ефективними. Етапи, що залишають кількість інваріантних збігів, дозволяють досліджувати простір пошуку, і з часом може відкритися деякий простір, щоб уникнути одного із збігів. Після 2 ^ 24 кроків без поліпшення попередній розмір виводиться на теперішнє значення n, а n збільшується.

Результати до n = 9 є 2, 4, 10, 16, 31, 47, 76, 112, 168, покращуючи попередні результати для n = 8 на одне і для n = 9 на два. Для більш високих значень n може бути збільшена межа 2 ^ 24 кроків.

Я також спробував імітувати відпал (з кількістю сірників як енергією), але без покращення в порівнянні з найбільш крутим спуском.

Код

зберегти як Question54354.java
компілювати з javac Question54354.java
запуском зjava 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
Дуже приємне поліпшення!

11

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

Примітки

Якщо ми розглянемо графік Gз вершинами 0до nта ребер, що з'єднують два числа, які відповідають, то сила тензора G^n має вершини, що (x_0, ..., x_{n-1})утворюють декартову потужність {0, ..., n}^nта ребра між збігаються кортежами. Графік інтересу - це підграф, G^n індукований тими вершинами, які відповідають можливим "підрахункам масивів".

Отже, перший підзадача - це генерування цих вершин. Наївний підхід перераховується через 2^{2n-1}рядки або на порядок 4^n. Але якщо ми замість цього подивимось на масив перших відмінностей підрахункових масивів, то виявимо, що існують лише 3^nможливості, і з перших відмінностей ми можемо вивести діапазон можливих початкових значень, вимагаючи, щоб жоден елемент у нульовій різниці не був меншим 0або більш чимn .

Потім ми хочемо знайти максимальний незалежний набір. Я використовую одну теорему та дві евристики:

  • Теорема: максимальний незалежний набір роз'єднаного об'єднання графіків - це об'єднання їх максимальних незалежних множин. Отже, якщо ми розбиваємо графік на непоєднані компоненти, ми можемо спростити проблему.
  • Евристичний: припустимо, що (n, n, ..., n)це буде в максимально незалежному наборі. Існує досить велика клаца вершин, {m, m+1, ..., n}^nде mнайменше ціле число, яке відповідає n;(n, n, ..., n)гарантовано не мати збігів поза цією клікою.
  • Евристичний: прийняти жадібний підхід до вибору вершини найнижчого ступеня.

На моєму комп'ютері це знаходить 111на n=8в 16 секунд, 166на n=9приблизно 8 хвилин, і в 235протягом n=10приблизно 2 годин.

Код

Зберегти як PPCG54354.java, скласти як javac PPCG54354.javaі запустити як 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

Математика n = 5,, 31 струна

Я щойно написав грубе рішення, використовуючи вбудовані програми Mathematica, щоб перевірити відповіді прикладу Лембіка, але він також може впоратися 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

Як бонус, цей код створює візуалізацію проблеми у вигляді графіка, де кожне ребро вказує на дві відповідні рядки.

Ось графік для n = 3:

введіть тут опис зображення


2
Спочатку я вважав, що графік добре симетричний, але потім я побачив злегка зміщену точку зліва. Не можете побачити :(
orlp

3

C ++ (евристичний): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086

Це трохи за результатом Пітера Тейлора, будучи 1 до 3 нижче для n=7, 9і 10. Перевага в тому, що це набагато швидше, тому я можу запустити його для більш високих значень n. І це можна зрозуміти без будь-якої фантазії математики. ;)

Поточний код розміщений для виконання n=15. Час виконання збільшиться приблизно в 4 рази за кожне збільшення n. Наприклад, це було 0,008 секунди n=7, 0,07 секунди n=9, 1,34 секунди n=11, 27 секунд n=13і 9 хвилин для n=15.

Я використовував два ключові спостереження:

  • Замість того, щоб оперувати самими значеннями, евристичний діє на підрахунок масивів. Для цього спочатку формується список усіх унікальних масивів підрахунку.
  • Використання підрахунку масивів з малими значеннями є більш вигідним, оскільки вони виключають менше простору рішення. Це ґрунтується на кожному підрахунку, cвиключаючи діапазон c / 2до 2 * cвід інших значень. Для менших значень c, цей діапазон є меншим, що означає, що менша кількість значень виключається при його використанні.

Створення унікальних масивів підрахунку

Я набрав грубу силу на цьому, повторюючи всі значення, генеруючи масив для кожного з них і уніфікувавши отриманий список. Це, безумовно, можна зробити більш ефективно, але це досить добре для типів цінностей, з якими ми працюємо.

Це надзвичайно швидко для малих значень. Для більших значень накладні витрати стають істотними. Наприклад, для n=15цього використовується близько 75% всього часу виконання. Це, безумовно, буде сфера, на яку слід звернути увагу, намагаючись вийти набагато вище, ніж n=15. Навіть просто використання пам'яті для складання списку лічильних масивів для всіх значень стане проблематичним.

Кількість унікальних масивів підрахунку становить близько 6% від кількості значень для n=15. Ця відносна кількість стає меншою, оскільки nстає більшою.

Жадібний вибір підрахунку значень масиву

Основна частина алгоритму вибирає підрахунок значень масиву зі створеного списку, використовуючи простий жадібний підхід.

Виходячи з теорії, що використання підрахунку масивів з невеликими підрахунками є вигідним, лічильні масиви сортуються за сумою їх підрахунків.

Потім вони перевіряються в порядку, і вибирається значення, якщо воно сумісне з усіма раніше використаними значеннями. Отже, це включає один єдиний лінійний прохід через унікальні масиви підрахунку, де кожен кандидат порівнюється зі значеннями, які були раніше обрані.

У мене є кілька ідей щодо того, як евристику можна потенційно вдосконалити. Але це здавалося розумною відправною точкою, і результати виглядали досить непогано.

Код

Це не дуже оптимізовано. У мене в якийсь момент була більш досконала структура даних, але для її узагальнення знадобилося б більше роботи n=8, і різниця в продуктивності не видалася дуже суттєвою.

#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;
}

У мене були рекурсивні рішення, засновані на подібному коді, які гарантовано знайдуть максимум. Але минув певний час n=4. Це, можливо, закінчилося б n=5з деяким терпінням. Напевно, ви використовували кращу стратегію зворотного відстеження, щоб навіть зробити це n=7. Чи був ваш евристичний, чи він досліджував весь простір рішення? Я розглядаю деякі ідеї, як зробити це кращим, або точним налаштуванням порядку сортування, або, можливо, не чисто жадібним.
Рето Коради

Я розумію, що у відповіді Пітера Тейлора немає зворотнього зв'язку. Це суто жадібно. Основні хитрощі полягають у тому, що він зменшує кількість підрахунків масивів 3^nі дві описані ним евристики.

@Lembik Мій коментар був у відповідь на коментар, який було видалено. Кількість підрахункових масивів має бути однаковою, оскільки я будую це на основі фактичних значень і зводя його до лише унікальних. Я оновив зворотну версію алгоритму. Незважаючи на те, що він не припиняється протягом розумного часу, він n=7швидко знаходить 76 . Але спробувавши це n=9, він все ще застряг у 164, коли я зупинив його через 20 хвилин. Таким чином, продовження цього обмеженою формою простого зворотного відстеження не виглядає загалом перспективним.
Рето Коради
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.