Temukan matriks skor tertinggi tanpa properti X


14

Tantangan ini sebagian merupakan tantangan algoritma, sebagian tantangan optimasi dan sebagian hanya tantangan kode tercepat.

Matriks siklik sepenuhnya ditentukan oleh baris pertama r. Baris yang tersisa adalah setiap permutasi siklik dari baris rdengan offset sama dengan indeks baris. Kami akan mengizinkan matriks siklik yang tidak persegi sehingga mereka hanya kehilangan beberapa baris terakhir mereka. Namun kami selalu menganggap bahwa jumlah baris tidak lebih dari jumlah kolom. Sebagai contoh, perhatikan 3 dari 5 matriks siklik berikut.

10111
11011
11101

Kami mengatakan sebuah matriks memiliki properti X jika berisi dua set kolom yang tidak kosong dengan indeks tidak identik yang memiliki jumlah (vektor) yang sama. Jumlah vektor dari dua kolom hanyalah penjumlahan elemen-bijaksana dari dua kolom. Itu adalah jumlah dari dua kolom yang mengandung xelemen masing-masing adalah kolom lain yang mengandung xelemen.

Matriks di atas sepele memiliki properti X karena kolom pertama dan terakhir adalah sama. Matriks identitas tidak pernah memiliki properti X.

Jika kita hanya menghapus kolom terakhir dari matriks di atas maka kita mendapatkan contoh yang tidak memiliki properti X dan akan memberikan skor 4/3.

1011
1101
1110

Tugas

Tugasnya adalah menulis kode untuk menemukan matriks siklik dengan skor tertinggi yang semua entri 0 atau 1 dan yang tidak memiliki properti X.

Skor

Skor Anda akan menjadi kolom jumlah dibagi dengan jumlah baris dalam matriks skor terbaik Anda.

Tie Breaker

Jika dua jawaban memiliki skor yang sama, jawaban yang pertama menang.

Dalam hal (sangat) tidak mungkin bahwa seseorang menemukan metode untuk mendapatkan skor tanpa batas, bukti valid pertama dari solusi semacam itu akan diterima. Dalam acara yang bahkan lebih tidak mungkin bahwa Anda dapat menemukan bukti optimalitas dari matriks yang terbatas saya tentu saja akan memberikan penghargaan juga.

Petunjuk

Mendapatkan skor 12/8 tidak terlalu sulit.

Bahasa dan perpustakaan

Anda dapat menggunakan bahasa apa pun yang memiliki kompiler / juru bahasa / dll yang tersedia secara bebas. untuk Linux dan semua perpustakaan yang juga tersedia secara bebas untuk Linux.

Entri terkemuka

  • 36/19 oleh Peter Taylor (Jawa)
  • 32/17 oleh Suboptimus Prime (C #)
  • 21/12 oleh justhalf (Python 2)

Ah, properti X ada di kolom, bukan baris.
Pengoptimal

Seperti yang ditulis, matriks 1 oleh 2 01 memiliki properti X karena himpunan kolom pertama memiliki jumlah vektor yang sama dengan himpunan kosong. Mungkin maksud Anda adalah kumpulan kolom kosong? Saya pikir lebih bersih untuk tidak mengubahnya.
xnor

2
Pembacaan termudah aturan masih yang 01memiliki properti X: (1) = (0) + (1). Jika Anda ingin mengecualikan itu maka Anda harus mengatakan bahwa dua set kolom harus dipisahkan.
Peter Taylor

1
Pertanyaan ini akan memberikan banyak wawasan tentang masalah ini (pada seberapa sulit untuk memeriksa properti X, yang merupakan NP-hard, sayangnya) mathoverflow.net/questions/157634/…
justhalf

3
Saat ini kami hanya memaksa semua 2^mkombinasi kolom untuk memeriksa properti X. Jika kami dapat merancang skema "bertemu di tengah" (lihat masalah "jumlah penjumlahan"), ini mungkin dapat mengurangi itu menjadi m * 2^(m/2)...
kennytm

Jawaban:


11

16/9 20/11 22/12 28/15 30/16 32/17 34/18 36/19 (Jawa)

Ini menggunakan sejumlah ide untuk mengurangi ruang pencarian dan biaya. Lihat riwayat revisi untuk detail lebih lanjut tentang versi kode yang lebih lama.

  • Jelas bahwa wlog kita hanya dapat mempertimbangkan matriks sirkuler di mana baris pertama adalah kata Lyndon : jika kata itu non-prima maka ia harus memiliki properti X, dan jika tidak kita dapat memutar tanpa mempengaruhi skor atau properti X.
  • Berdasarkan heuristik dari pemenang pendek yang diamati, saya sekarang mengulangi kata-kata Lyndon mulai dari yang dengan kepadatan 50% (yaitu jumlah yang sama dengan 0dan 1) dan bekerja; Saya menggunakan algoritma yang dijelaskan dalam kode A Gray untuk kalung dengan kepadatan tetap dan kata-kata Lyndon dalam waktu diamortisasi konstan , Sawada dan Williams, Theoretical Computer Science 502 (2013): 46-54.
  • Pengamatan empiris adalah bahwa nilai-nilai terjadi berpasangan: setiap kata Lyndon optimal yang saya temukan skor sama dengan pembalikannya. Jadi saya mendapatkan faktor dua percepatan dengan hanya mempertimbangkan satu setengah dari masing-masing pasangan tersebut.
  • Kode asli saya berfungsi BigIntegeruntuk memberikan tes yang tepat. Saya mendapatkan peningkatan kecepatan yang signifikan, dengan risiko negatif palsu, dengan mengoperasikan modulo prima besar dan menjaga semuanya tetap primitif. Perdana yang saya pilih adalah yang terbesar lebih kecil dari 2 57 , karena memungkinkan mengalikan dengan basis representasi vektor nosional saya tanpa meluap.
  • Saya telah mencuri heuristik Suboptimus Prime yang memungkinkan untuk mendapatkan penolakan cepat dengan mempertimbangkan himpunan bagian dalam urutan ukuran yang meningkat. Sekarang saya telah menggabungkan ide itu dengan pendekatan terner subset meet-in-the-middle untuk menguji colliding subset. (Kredit untuk KennyTM untuk menyarankan mencoba mengadaptasi pendekatan dari masalah subset integer; Saya pikir xnor dan saya melihat cara untuk melakukannya cukup banyak secara bersamaan). Daripada mencari dua himpunan bagian yang dapat mencakup setiap kolom 0 atau 1 kali dan memiliki jumlah yang sama, kami mencari satu subset yang dapat mencakup setiap kolom -1, 0, atau 1 kali dan jumlah ke nol. Ini secara signifikan mengurangi kebutuhan memori.
  • Ada faktor tambahan dari dua penghematan dalam persyaratan memori dengan mengamati bahwa karena setiap elemen {-1,0,1}^mmemiliki negasi juga di {-1,0,1}^mdalamnya hanya perlu untuk menyimpan salah satu dari keduanya.
  • Saya juga meningkatkan persyaratan dan kinerja memori dengan menggunakan implementasi hashmap khusus. Untuk menguji 36/19 membutuhkan penyimpanan 3 ^ 18 jumlah, dan 3 ^ 18 panjang hampir 3GB tanpa overhead - saya memberikannya 6GB tumpukan karena 4GB tidak cukup; untuk melangkah lebih jauh (mis. tes 38/20) dalam 8GB RAM akan memerlukan optimasi lebih lanjut untuk menyimpan int daripada yang lama. Dengan 20 bit diperlukan untuk mengatakan subset mana yang menghasilkan jumlah yang akan meninggalkan 12 bit ditambah bit implisit dari bucket; Saya takut akan ada terlalu banyak tabrakan palsu untuk mendapatkan hit.
  • Karena bobot bukti menunjukkan bahwa kita harus melihat 2n/(n+1), saya mempercepat dengan hanya menguji itu.
  • Ada beberapa hasil statistik yang tidak perlu tetapi meyakinkan.
import java.util.*;

// Aiming to find a solution for (2n, n+1).
public class PPCG41021_QRTernary_FixedDensity {
    private static final int N = 36;
    private static int density;
    private static long start;
    private static long nextProgressReport;

    public static void main(String[] args) {
        start = System.nanoTime();
        nextProgressReport = start + 5 * 60 * 1000000000L;

        // 0, -1, 1, -2, 2, ...
        for (int i = 0; i < N - 1; i++) {
            int off = i >> 1;
            if ((i & 1) == 1) off = ~off;
            density = (N >> 1) + off;

            // Iterate over Lyndon words of length N and given density.
            for (int j = 0; j < N; j++) a[j] = j < N - density ? '0' : '1';
            c = 1;
            Bs[1] = N - density;
            Bt[1] = density;
            gen(N - density, density, 1);
            System.out.println("----");
        }

        System.out.println("Finished in " + (System.nanoTime() - start)/1000000 + " ms");
    }

    private static int c;
    private static int[] Bs = new int[N + 1], Bt = new int[N + 1];
    private static char[] a = new char[N];
    private static void gen(int s, int t, int r) {
        if (s > 0 && t > 0) {
            int j = oracle(s, t, r);
            for (int i = t - 1; i >= j; i--) {
                updateBlock(s, t, i);
                char tmp = a[s - 1]; a[s - 1] = a[s+t-i - 1]; a[s+t-i - 1] = tmp;
                gen(s-1, t-i, testSuffix(r) ? c-1 : r);
                tmp = a[s - 1]; a[s - 1] = a[s+t-i - 1]; a[s+t-i - 1] = tmp;
                restoreBlock(s, t, i);
            }
        }
        visit();
    }

    private static int oracle(int s, int t, int r) {
        int j = pseudoOracle(s, t, r);
        updateBlock(s, t, j);
        int p = testNecklace(testSuffix(r) ? c - 1 : r);
        restoreBlock(s, t, j);
        return p == N ? j : j + 1;
    }

    private static int pseudoOracle(int s, int t, int r) {
        if (s == 1) return t;
        if (c == 1) return s == 2 ? N / 2 : 1;
        if (s - 1 > Bs[r] + 1) return 0;
        if (s - 1 == Bs[r] + 1) return cmpPair(s-1, t, Bs[c-1]+1, Bt[c-1]) <= 0 ? 0 : 1;
        if (s - 1 == Bs[r]) {
            if (s == 2) return Math.max(t - Bt[r], (t+1) >> 1);
            return Math.max(t - Bt[r], (cmpPair(s-1, t, Bs[c-1] + 1, Bt[c-1]) <= 0) ? 0 : 1); 
        }
        if (s == Bs[r]) return t;
        throw new UnsupportedOperationException("Hit the case not covered by the paper or its accompanying code");
    }

    private static int testNecklace(int r) {
        if (density == 0 || density == N) return 1;
        int p = 0;
        for (int i = 0; i < c; i++) {
            if (r - i <= 0) r += c;
            if (cmpBlocks(c-i, r-i) < 0) return 0;
            if (cmpBlocks(c-i, r-1) > 0) return N;
            if (r < c) p += Bs[r-i] + Bt[r-i];
        }
        return p;
    }

    private static int cmpPair(int a1, int a2, int b1, int b2) {
        if (a1 < b1) return -1;
        if (a1 > b1) return 1;
        if (a2 < b2) return -1;
        if (a2 > b2) return 1;
        return 0;
    }

    private static int cmpBlocks(int i, int j) {
        return cmpPair(Bs[i], Bt[i], Bs[j], Bt[j]);
    }

    private static boolean testSuffix(int r) {
        for (int i = 0; i < r; i++) {
            if (c - 1 - i == r) return true;
            if (cmpBlocks(c-1-i, r-i) < 0) return false;
            if (cmpBlocks(c-1-i, r-1) > 0) return true;
        }
        return false;
    }

    private static void updateBlock(int s, int t, int i) {
        if (i == 0 && c > 1) {
            Bs[c-1]++;
            Bs[c] = s - 1;
        }
        else {
            Bs[c] = 1;
            Bt[c] = i;
            Bs[c+1] = s-1;
            Bt[c+1] = t-i;
            c++;
        }
    }

    private static void restoreBlock(int s, int t, int i) {
        if (i == 0 && (c > 0 || (Bs[1] != 1 || Bt[1] != 0))) {
            Bs[c-1]--;
            Bs[c] = s;
        }
        else {
            Bs[c-1] = s;
            Bt[c-1] = t;
            c--;
        }
    }

    private static long[] stats = new long[N/2+1];
    private static long visited = 0;
    private static void visit() {
        String word = new String(a);

        visited++;
        if (precedesReversal(word) && testTernary(word)) System.out.println(word + " after " + (System.nanoTime() - start)/1000000 + " ms");
        if (System.nanoTime() > nextProgressReport) {
            System.out.println("Progress: visited " + visited + "; stats " + Arrays.toString(stats) + " after " + (System.nanoTime() - start)/1000000 + " ms");
             nextProgressReport += 5 * 60 * 1000000000L;
        }
    }

    private static boolean precedesReversal(String w) {
        int n = w.length();
        StringBuilder rev = new StringBuilder(w);
        rev.reverse();
        rev.append(rev, 0, n);
        for (int i = 0; i < n; i++) {
            if (rev.substring(i, i + n).compareTo(w) < 0) return false;
        }
        return true;
    }

    private static boolean testTernary(String word) {
        int n = word.length();
        String rep = word + word;

        int base = 1;
        for (char ch : word.toCharArray()) base += ch & 1;

        // Operating base b for b up to 32 implies that we can multiply by b modulo p<2^57 without overflowing a long.
        // We're storing 3^(n/2) ~= 2^(0.8*n) sums, so while n < 35.6 we don't get *too* bad a probability of false reject.
        // (In fact the birthday paradox assumes independence, and our values aren't independent, so we're better off than that).
        long p = (1L << 57) - 13;
        long[] basis = new long[n];
        basis[0] = 1;
        for (int i = 1; i < basis.length; i++) basis[i] = (basis[i-1] * base) % p;

        int rows = n / 2 + 1;
        long[] colVals = new long[n];
        for (int col = 0; col < n; col++) {
            for (int row = 0; row < rows; row++) {
                colVals[col] = (colVals[col] + basis[row] * (rep.charAt(row + col) & 1)) % p;
            }
        }

        MapInt57Int27 map = new MapInt57Int27();
        // Special-case the initial insertion.
        int[] oldLens = new int[map.entries.length];
        int[] oldSupercounts = new int[1 << 10];
        {
            // count = 1
            for (int k = 0; k < n/2; k++) {
                int val = 1 << (25 - k);
                if (!map.put(colVals[k], val)) { stats[1]++; return false; }
                if (!map.put(colVals[k + n/2], val + (1 << 26))) { stats[1]++; return false; }
            }
        }
        final long keyMask = (1L << 37) - 1;
        for (int count = 2; count <= n/2; count++) {
            int[] lens = map.counts.clone();
            int[] supercounts = map.supercounts.clone();
            for (int sup = 0; sup < 1 << 10; sup++) {
                int unaccountedFor = supercounts[sup] - oldSupercounts[sup];
                for (int supi = 0; supi < 1 << 10 && unaccountedFor > 0; supi++) {
                    int i = (sup << 10) + supi;
                    int stop = lens[i];
                    unaccountedFor -= stop - oldLens[i];
                    for (int j = oldLens[i]; j < stop; j++) {
                        long existingKV = map.entries[i][j];
                        long existingKey = ((existingKV & keyMask) << 20) + i;
                        int existingVal = (int)(existingKV >>> 37);

                        // For each possible prepend...
                        int half = (existingVal >> 26) * n/2;
                        // We have 27 bits of key, of which the top marks the half, so 26 bits. That means there are 6 bits at the top which we need to not count.
                        int k = Integer.numberOfLeadingZeros(existingVal << 6) - 1;
                        while (k >= 0) {
                            int newVal = existingVal | (1 << (25 - k));
                            long pos = (existingKey + colVals[k + half]) % p;
                            if (pos << 1 > p) pos = p - pos;
                            if (pos == 0 || !map.put(pos, newVal)) { stats[count]++; return false; }
                            long neg = (p - existingKey + colVals[k + half]) % p;
                            if (neg << 1 > p) neg = p - neg;
                            if (neg == 0 || !map.put(neg, newVal)) { stats[count]++; return false; }
                            k--;
                        }
                    }
                }
            }
            oldLens = lens;
            oldSupercounts = supercounts;
        }

        stats[n/2]++;
        return true;
    }

    static class MapInt57Int27 {
        private long[][] entries;
        private int[] counts;
        private int[] supercounts;

        public MapInt57Int27() {
            entries = new long[1 << 20][];
            counts = new int[1 << 20];
            supercounts = new int[1 << 10];
        }

        public boolean put(long key, int val) {
            int bucket = (int)(key & (entries.length - 1));
            long insert = (key >>> 20) | (((long)val) << 37);
            final long mask = (1L << 37) - 1;

            long[] chain = entries[bucket];
            if (chain == null) {
                chain = new long[16];
                entries[bucket] = chain;
                chain[0] = insert;
                counts[bucket]++;
                supercounts[bucket >> 10]++;
                return true;
            }

            int stop = counts[bucket];
            for (int i = 0; i < stop; i++) {
                if ((chain[i] & mask) == (insert & mask)) {
                    return false;
                }
            }

            if (stop == chain.length) {
                long[] newChain = new long[chain.length < 512 ? chain.length << 1 : chain.length + 512];
                System.arraycopy(chain, 0, newChain, 0, chain.length);
                entries[bucket] = newChain;
                chain = newChain;
            }
            chain[stop] = insert;
            counts[bucket]++;
            supercounts[bucket >> 10]++;
            return true;
        }
    }
}

Yang pertama ditemukan adalah

000001001010110001000101001111111111

dan itu satu-satunya hit dalam 15 jam.

Pemenang yang lebih kecil:

4/3:    0111                       (plus 8 different 8/6)
9/6:    001001011                  (and 5 others)
11/7:   00010100111                (and 3 others)
13/8:   0001001101011              (and 5 others)
15/9:   000010110110111            (and 21 others)
16/9:   0000101110111011           (and 1 other)
20/11:  00000101111011110111       (and others)
22/12:  0000001100110011101011     (and others)
24/13:  000000101011101011101011   (and others)
26/14:  00000001101110010011010111 (and others)
28/15:  0000000010000111100111010111 (and others)
30/16:  000000001011001110011010101111 (and probably others)
32/17:  00001100010010100100101011111111 (and others)
34/18:  0000101000100101000110010111111111 (and others)

Ini peningkatan yang bagus. Tampaknya menggunakan kata-kata Lyndon berarti Anda hanya perlu memeriksa kira-kira 2 ^ n / n string biner untuk baris pertama, bukan 2 ^ n.

Karena Anda menggunakan setiap digit BigInteger sebagai sel matriks, tidak akankah ada jawaban yang salah ketika n> 10?
kennytm

@ KennyTM, perhatikan bahwa parameter kedua adalah radix. Ada bug kecil: Saya harus menggunakan ndaripada rows, meskipun gagal-aman dalam arti bahwa itu akan membuang solusi yang valid daripada menerima yang tidak valid. Itu juga tidak mempengaruhi hasil.
Peter Taylor

1
Saya pikir kami praktis terbatas pada skor ini, karena pemeriksaan properti X sangat memakan waktu, kecuali kami menemukan kondisi lain yang setara yang dapat dievaluasi lebih cepat. Itu sebabnya saya sangat ingin melihat bahwa "non-prime" menyiratkan properti X = D
justhalf

1
@SuboptimusPrime, saya menemukannya di people.math.sfu.ca/~kya17/teaching/math343/16-343.pdf dan memperbaiki bug. Menariknya, algoritma yang sekarang saya gunakan untuk beralih melalui kata-kata Lyndon adalah salah satu dari kelas algoritma terkait yang juga melakukan himpunan bagian k-of-n, jadi saya mungkin dapat melakukan refactor dan berbagi beberapa kode.
Peter Taylor

9

Python 2 - 21/12

Dalam proses membuktikan bahwa 2-(3/n)selalu ada untuk apa punn

Terinspirasi oleh pertanyaan ini , saya menggunakan Sequence De Bruijn untuk secara paksa memaksa matriks yang mungkin. Dan setelah bruteforcing n=6,7,8,9,10, saya menemukan pola bahwa solusi tertinggi selalu dalam bentuk (n, 2n-3).

Jadi saya membuat metode lain untuk memperkuat bentuk matriks itu, dan menggunakan multiprosesor untuk mempercepat, karena tugas ini sangat dapat didistribusikan. Di ubuntu 16-inti, ia dapat menemukan solusi n=12sekitar 4 menit:

Mencoba (0, 254)
Mencoba (254, 509)
Mencoba (509, 764)
Mencoba (764, 1018)
Mencoba (1018, 1273)
Mencoba (1273, 1528)
Mencoba (1528, 1782)
Mencoba (1782, 2037)
Mencoba (2037, 2292)
Mencoba (2292, 2546)
Mencoba (2546, 2801)
Mencoba (2801, 3056)
Mencoba (3056, 3310)
Mencoba (3820, 4075)
Mencoba (3565, 3820)
Mencoba (3310, 3565)
(1625, 1646)
[[0 0 0 1 0 0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0]
 [0 0 1 0 0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0]
 [0 1 0 0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0]
 [1 0 0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0]
 [0 0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 1]
 [0 1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0]
 [1 0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0]
 [0 1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1]
 [1 1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1 0]
 [1 1 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1 0 1]
 [1 1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1 0 1 1]
 [1 0 0 0 1 0 0 1 1 0 0 0 0 1 0 0 1 0 1 1 1]]
(12, 21)
Nilai: 1,7500

4m9.121s nyata
pengguna 42m47.472s
sys 0m5.780s

Sebagian besar perhitungan digunakan untuk memeriksa properti X, yang mengharuskan memeriksa semua himpunan bagian (ada 2^(2n-3)himpunan bagian)

Perhatikan bahwa saya memutar baris pertama ke kiri, bukan ke kanan seperti pada pertanyaan. Tapi ini setara karena Anda bisa membalikkan seluruh matriks. =)

Kode:

import math
import numpy as np
from itertools import combinations
from multiprocessing import Process, Queue, cpu_count

def de_bruijn(k, n):
    """
    De Bruijn sequence for alphabet k
    and subsequences of length n.
    """
    alphabet = list(range(k))
    a = [0] * k * n
    sequence = []
    def db(t, p):
        if t > n:
            if n % p == 0:
                for j in range(1, p + 1):
                    sequence.append(a[j])
        else:
            a[t] = a[t - p]
            db(t + 1, p)
            for j in range(a[t - p] + 1, k):
                a[t] = j
                db(t + 1, t)
    db(1, 1)
    return sequence

def generate_cyclic_matrix(seq, n):
    result = []
    for i in range(n):
        result.append(seq[i:]+seq[:i])
    return np.array(result)

def generate_cyclic_matrix_without_property_x(n=3, n_jobs=-1):
    seq = de_bruijn(2,n)
    seq = seq + seq[:n/2]
    max_idx = len(seq)
    max_score = 1
    max_matrix = np.array([[]])
    max_ij = (0,0)
    workers = []
    queue = Queue()
    if n_jobs < 0:
        n_jobs += cpu_count()+1
    for i in range(n_jobs):
        worker = Process(target=worker_function, args=(seq,i*(2**n-2*n+3)/n_jobs, (i+1)*(2**n-2*n+3)/n_jobs, n, queue))
        workers.append(worker)
        worker.start()
    (result, max_ij) = queue.get()
    for worker in workers:
        worker.terminate()
    return (result, max_ij)

def worker_function(seq,min_idx,max_idx,n,queue):
    print 'Trying (%d, %d)' % (min_idx, max_idx)
    for i in range(min_idx, max_idx):
        j = i+2*n-3
        result = generate_cyclic_matrix(seq[i:j], n)
        if has_property_x(result):
            continue
        else:
            queue.put( (result, (i,j)) )
            return

def has_property_x(mat):
    vecs = zip(*mat)
    vector_sums = set()
    for i in range(1, len(vecs)+1):
        for combination in combinations(vecs, i):
            vector_sum = tuple(sum(combination, np.array([0]*len(mat))))
            if vector_sum in vector_sums:
                return True
            else:
                vector_sums.add(vector_sum)
    return False

def main():
    import sys
    n = int(sys.argv[1])
    if len(sys.argv) > 2:
        n_jobs = int(sys.argv[2])
    else:
        n_jobs = -1
    (matrix, ij) = generate_cyclic_matrix_without_property_x(n, n_jobs)
    print ij
    print matrix
    print matrix.shape
    print 'Score: %.4f' % (float(matrix.shape[1])/matrix.shape[0])

if __name__ == '__main__':
    main()

Jawaban lama, untuk referensi

Solusi optimal sejauh ini ( n=10):

(855, 872)
[[1 1 0 1 0 1 0 0 1 1 1 1 0 1 1 1 0]
 [1 0 1 0 1 0 0 1 1 1 1 0 1 1 1 0 1]
 [0 1 0 1 0 0 1 1 1 1 0 1 1 1 0 1 1]
 [1 0 1 0 0 1 1 1 1 0 1 1 1 0 1 1 0]
 [0 1 0 0 1 1 1 1 0 1 1 1 0 1 1 0 1]
 [1 0 0 1 1 1 1 0 1 1 1 0 1 1 0 1 0]
 [0 0 1 1 1 1 0 0 1 1 0 0 1 1 0 1 0 1]
 [0 1 1 1 1 0 0 1 1 0 0 1 1 0 1 0 1 0]
 [1 1 1 1 0 0 1 1 0 0 1 1 0 1 0 1 0 0]
 [1 1 1 0 1 1 1 0 1 1 0 1 0 1 0 0 1]]
(10, 17)
Nilai: 1,7000

Untuk n=7:

(86, 97)
[[0 1 1 1 0 0 0 0 1 1 1]
 [1 1 1 0 1 0 0 1 1 1 0]
 [1 1 0 1 0 0 1 1 1 0 1]
 [1 0 1 0 0 1 1 1 0 1 1]
 [0 1 0 0 1 1 1 0 1 1 1]
 [1 0 0 1 1 1 0 1 1 1 0]
 [0 0 1 1 1 0 0 1 1 0 0]]
(7, 11)
Nilai: 1,5714

Solusi dengan bentuk seperti yang dijelaskan oleh OP ( n=8):

(227, 239)
[[0 1 0 1 1 1 1 1 0 1 1 0]
 [1 0 1 1 1 1 1 0 0 1 1 0 0]
 [0 1 1 1 1 1 0 0 1 1 0 0 1]
 [1 1 1 1 1 0 0 1 1 0 0 1 0]
 [1 1 1 1 0 0 1 1 0 0 1 0 1]
 [1 1 1 0 1 1 0 0 1 0 1 1]
 [1 1 0 1 1 0 0 1 0 1 1 1]
 [1 0 1 1 0 0 1 0 1 1 1 1]]
(8, 12)
Nilai: 1,5000

Tapi yang lebih baik ( n=8):

(95, 108)
[[0 1 1 0 0 1 0 0 0 1 1 0 1]
 [1 1 0 0 1 0 0 0 1 1 0 1 0]
 [1 0 0 1 0 0 0 1 1 0 1 0 1]
 [0 0 1 0 0 0 1 1 0 1 0 1 1]
 [0 1 0 0 0 1 1 0 1 0 1 1 0]
 [1 0 0 0 1 1 0 1 0 1 1 0 0]
 [0 0 0 1 1 0 0 0 0 1 1 0 0 1]
 [0 0 1 1 0 1 0 1 1 0 0 1 0]]
(8, 13)
Nilai: 1,6250

Ini juga menemukan solusi optimal lain di n=9:

(103, 118)
[[0 1 0 1 1 1 0 0 0 0 1 1 0 0 1]
 [1 0 1 1 1 0 0 0 0 1 1 0 0 1 0]
 [0 1 1 1 0 0 0 0 1 1 0 0 1 0 1]
 [1 1 1 0 0 0 0 1 1 0 0 1 0 1 0]
 [1 1 0 0 0 0 1 1 0 0 1 0 1 0 1]
 [1 0 0 0 0 1 1 0 0 1 0 1 0 1 1]
 [0 0 0 0 1 1 0 0 1 0 1 0 1 1 1]
 [0 0 0 1 1 0 0 1 0 1 0 1 1 1 0]
 [0 0 1 1 0 0 1 0 1 0 1 1 1 0 0]]
(9, 15)
Nilai: 1,6667

Kode tersebut adalah sebagai berikut. Itu hanya kekerasan, tapi setidaknya itu bisa menemukan sesuatu yang lebih baik daripada klaim OP =)

import numpy as np
from itertools import combinations

def de_bruijn(k, n):
    """
    De Bruijn sequence for alphabet k
    and subsequences of length n.
    """
    alphabet = list(range(k))
    a = [0] * k * n
    sequence = []
    def db(t, p):
        if t > n:
            if n % p == 0:
                for j in range(1, p + 1):
                    sequence.append(a[j])
        else:
            a[t] = a[t - p]
            db(t + 1, p)
            for j in range(a[t - p] + 1, k):
                a[t] = j
                db(t + 1, t)
    db(1, 1)
    return sequence

def generate_cyclic_matrix(seq, n):
    result = []
    for i in range(n):
        result.append(seq[i:]+seq[:i])
    return np.array(result)

def generate_cyclic_matrix_without_property_x(n=3):
    seq = de_bruijn(2,n)
    max_score = 0
    max_matrix = []
    max_ij = (0,0)
    for i in range(2**n+1):
        for j in range(i+n, 2**n+1):
            score = float(j-i)/n
            if score <= max_score:
                continue
            result = generate_cyclic_matrix(seq[i:j], n)
            if has_property_x(result):
                continue
            else:
                if score > max_score:
                    max_score = score
                    max_matrix = result
                    max_ij = (i,j)
    return (max_matrix, max_ij)

def has_property_x(mat):
    vecs = zip(*mat)
    vector_sums = set()
    for i in range(1, len(vecs)):
        for combination in combinations(vecs, i):
            vector_sum = tuple(sum(combination, np.array([0]*len(mat))))
            if vector_sum in vector_sums:
                return True
            else:
                vector_sums.add(vector_sum)
    return False

def main():
    import sys
    n = int(sys.argv[1])
    (matrix, ij) = generate_cyclic_matrix_without_property_x(n)
    print ij
    print matrix
    print matrix.shape
    print 'Score: %.4f' % (float(matrix.shape[1])/matrix.shape[0])

if __name__ == '__main__':
    main()

Awal yang bagus :)

2
@Lembik Sekarang saya dapat mengalahkan hampir (dibatasi oleh waktu komputasi) siapa pun yang mengklaim skor apa pun di bawah 2. =)
justhalf

Kalau begitu, bisakah Anda mengalahkan 19/10?

@Lembik, kupikir aku tidak bisa. Ini membutuhkan n >= 31, yang menyiratkan saya perlu memeriksa 2^(2n-3) = 2^59kombinasi vektor 31-dimensi. Tidak akan selesai dalam hidup kita = D
justhalf

2
Bisakah Anda membuktikan bahwa Anda selalu bisa mendapatkan matriksn*(2n-3)
xnor

7

24/13 26/14 28/15 30/16 32/17 (C #)

Sunting: Info lama yang dihapus dari jawaban saya. Saya menggunakan sebagian besar algoritma yang sama dengan Peter Taylor ( Edit: sepertinya dia menggunakan algoritma yang lebih baik sekarang), walaupun saya telah menambahkan beberapa optimasi saya sendiri:

  • Saya telah menerapkan strategi "bertemu di tengah" untuk mencari set kolom dengan jumlah vektor yang sama (disarankan oleh komentar KennyTM ini ). Strategi ini banyak meningkatkan penggunaan memori, tetapi agak lambat, jadi saya telah menambahkan HasPropertyXFastfungsinya, yang dengan cepat memeriksa apakah ada set kecil dengan jumlah yang sama sebelum menggunakan pendekatan "bertemu di tengah".
  • Sementara iterasi melalui set kolom dalam HasPropertyXFast fungsi, saya mulai dari memeriksa set kolom dengan 1 kolom, kemudian dengan 2, 3 dan seterusnya. Fungsi kembali segera setelah tabrakan jumlah kolom pertama ditemukan. Dalam praktiknya itu berarti bahwa saya biasanya harus memeriksa hanya beberapa ratus atau ribuan set kolom daripada jutaan.
  • Saya menggunakan longvariabel untuk menyimpan dan membandingkan seluruh kolom dan jumlah vektornya. Pendekatan ini setidaknya urutan besarnya lebih cepat daripada membandingkan kolom sebagai array.
  • Saya telah menambahkan implementasi hashset saya sendiri, dioptimalkan untuk long tipe data dan untuk pola penggunaan saya.
  • Saya menggunakan kembali 3 hash yang sama untuk seumur hidup aplikasi untuk mengurangi jumlah alokasi memori dan meningkatkan kinerja.
  • Dukungan multithreading.

Output program:

00000000000111011101010010011111
10000000000011101110101001001111
11000000000001110111010100100111
11100000000000111011101010010011
11110000000000011101110101001001
11111000000000001110111010100100
01111100000000000111011101010010
00111110000000000011101110101001
10011111000000000001110111010100
01001111100000000000111011101010
00100111110000000000011101110101
10010011111000000000001110111010
01001001111100000000000111011101
10100100111110000000000011101110
01010010011111000000000001110111
10101001001111100000000000111011
11010100100111110000000000011101
Score: 32/17 = 1,88235294117647
Time elapsed: 02:11:05.9791250

Kode:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class Program
{
    const int MaxWidth = 32;
    const int MaxHeight = 17;

    static object _lyndonWordLock = new object();

    static void Main(string[] args)
    {
        Stopwatch sw = Stopwatch.StartNew();
        double maxScore = 0;
        const int minHeight = 17; // 1
        for (int height = minHeight; height <= MaxHeight; height++)
        {
            Console.WriteLine("Row count = " + height);
            Console.WriteLine("Time elapsed: " + sw.Elapsed + "\r\n");

            int minWidth = Math.Max(height, (int)(height * maxScore) + 1);
            for (int width = minWidth; width <= MaxWidth; width++)
            {
#if MULTITHREADING
                int[,] matrix = FindMatrixParallel(width, height);
#else
                int[,] matrix = FindMatrix(width, height);
#endif
                if (matrix != null)
                {
                    PrintMatrix(matrix);
                    Console.WriteLine("Time elapsed: " + sw.Elapsed + "\r\n");
                    maxScore = (double)width / height;
                }
                else
                    break;
            }
        }
    }

#if MULTITHREADING
    static int[,] FindMatrixParallel(int width, int height)
    {
        _lyndonWord = 0;
        _stopSearch = false;

        int threadCount = Environment.ProcessorCount;
        Task<int[,]>[] tasks = new Task<int[,]>[threadCount];
        for (int i = 0; i < threadCount; i++)
            tasks[i] = Task<int[,]>.Run(() => FindMatrix(width, height));

        int index = Task.WaitAny(tasks);
        if (tasks[index].Result != null)
            _stopSearch = true;

        Task.WaitAll(tasks);
        foreach (Task<int[,]> task in tasks)
            if (task.Result != null)
                return task.Result;

        return null;
    }

    static volatile bool _stopSearch;
#endif

    static int[,] FindMatrix(int width, int height)
    {
#if MULTITHREADING
        _columnSums = new LongSet();
        _left = new LongSet();
        _right = new LongSet();
#endif

        foreach (long rowTemplate in GetLyndonWords(width))
        {
            int[,] matrix = new int[width, height];
            for (int x = 0; x < width; x++)
            {
                int cellValue = (int)(rowTemplate >> (width - 1 - x)) % 2;
                for (int y = 0; y < height; y++)
                    matrix[(x + y) % width, y] = cellValue;
            }

            if (!HasPropertyX(matrix))
                return matrix;

#if MULTITHREADING
            if (_stopSearch)
                return null;
#endif
        }

        return null;
    }

#if MULTITHREADING
    static long _lyndonWord;
#endif

    static IEnumerable<long> GetLyndonWords(int length)
    {
        long lyndonWord = 0;
        long max = (1L << (length - 1)) - 1;
        while (lyndonWord <= max)
        {
            if ((lyndonWord % 2 != 0) && PrecedesReversal(lyndonWord, length))
                yield return lyndonWord;

#if MULTITHREADING
            lock (_lyndonWordLock)
            {
                if (_lyndonWord <= max)
                    _lyndonWord = NextLyndonWord(_lyndonWord, length);
                else
                    yield break;

                lyndonWord = _lyndonWord;
            }
#else
            lyndonWord = NextLyndonWord(lyndonWord, length);
#endif
        }
    }

    static readonly int[] _lookup =
    {
        32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4, 7, 17,
        0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5, 20, 8, 19, 18
    };

    static int NumberOfTrailingZeros(uint i)
    {
        return _lookup[(i & -i) % 37];
    }

    static long NextLyndonWord(long w, int length)
    {
        if (w == 0)
            return 1;

        int currentLength = length - NumberOfTrailingZeros((uint)w);
        while (currentLength < length)
        {
            w += w >> currentLength;
            currentLength *= 2;
        }

        w++;

        return w;
    }

    private static bool PrecedesReversal(long lyndonWord, int length)
    {
        int shift = length - 1;

        long reverse = 0;
        for (int i = 0; i < length; i++)
        {
            long bit = (lyndonWord >> i) % 2;
            reverse |= bit << (shift - i);
        }

        for (int i = 0; i < length; i++)
        {
            if (reverse < lyndonWord)
                return false;

            long bit = reverse % 2;
            reverse /= 2;
            reverse += bit << shift;
        }

        return true;
    }

#if MULTITHREADING
    [ThreadStatic]
#endif
    static LongSet _left = new LongSet();
#if MULTITHREADING
    [ThreadStatic]
#endif
    static LongSet _right = new LongSet();

    static bool HasPropertyX(int[,] matrix)
    {
        long[] matrixColumns = GetMatrixColumns(matrix);
        if (matrixColumns.Length == 1)
            return false;

        return HasPropertyXFast(matrixColumns) || MeetInTheMiddle(matrixColumns);
    }

    static bool MeetInTheMiddle(long[] matrixColumns)
    {
        long[] leftColumns = matrixColumns.Take(matrixColumns.Length / 2).ToArray();
        long[] rightColumns = matrixColumns.Skip(matrixColumns.Length / 2).ToArray();

        if (PrepareHashSet(leftColumns, _left) || PrepareHashSet(rightColumns, _right))
            return true;

        foreach (long columnSum in _left.GetValues())
            if (_right.Contains(columnSum))
                return true;

        return false;
    }

    static bool PrepareHashSet(long[] columns, LongSet sums)
    {
        int setSize = (int)System.Numerics.BigInteger.Pow(3, columns.Length);
        sums.Reset(setSize, setSize);
        foreach (long column in columns)
        {
            foreach (long sum in sums.GetValues())
                if (!sums.Add(sum + column) || !sums.Add(sum - column))
                    return true;

            if (!sums.Add(column) || !sums.Add(-column))
                return true;
        }

        return false;
    }

#if MULTITHREADING
    [ThreadStatic]
#endif
    static LongSet _columnSums = new LongSet();

    static bool HasPropertyXFast(long[] matrixColumns)
    {
        int width = matrixColumns.Length;

        int maxColumnCount = width / 3;
        _columnSums.Reset(width, SumOfBinomialCoefficients(width, maxColumnCount));

        int resetBit, setBit;
        for (int k = 1; k <= maxColumnCount; k++)
        {
            uint columnMask = (1u << k) - 1;
            long sum = 0;
            for (int i = 0; i < k; i++)
                sum += matrixColumns[i];

            while (true)
            {
                if (!_columnSums.Add(sum))
                    return true;
                if (!NextColumnMask(columnMask, k, width, out resetBit, out setBit))
                    break;
                columnMask ^= (1u << resetBit) ^ (1u << setBit);
                sum = sum - matrixColumns[resetBit] + matrixColumns[setBit];
            }
        }

        return false;
    }

    // stolen from Peter Taylor
    static bool NextColumnMask(uint mask, int k, int n, out int resetBit, out int setBit)
    {
        int gap = NumberOfTrailingZeros(~mask);
        int next = 1 + NumberOfTrailingZeros(mask & (mask + 1));

        if (((k - gap) & 1) == 0)
        {
            if (gap == 0)
            {
                resetBit = next - 1;
                setBit = next - 2;
            }
            else if (gap == 1)
            {
                resetBit = 0;
                setBit = 1;
            }
            else
            {
                resetBit = gap - 2;
                setBit = gap;
            }
        }
        else
        {
            if (next == n)
            {
                resetBit = 0;
                setBit = 0;
                return false;
            }

            if ((mask & (1 << next)) == 0)
            {
                if (gap == 0)
                {
                    resetBit = next - 1;
                    setBit = next;
                }
                else
                {
                    resetBit = gap - 1;
                    setBit = next;
                }
            }
            else
            {
                resetBit = next;
                setBit = gap;
            }
        }

        return true;
    }

    static long[] GetMatrixColumns(int[,] matrix)
    {
        int width = matrix.GetLength(0);
        int height = matrix.GetLength(1);

        long[] result = new long[width];
        for (int x = 0; x < width; x++)
        {
            long column = 0;
            for (int y = 0; y < height; y++)
            {
                column *= 13;
                if (matrix[x, y] == 1)
                    column++;
            }

            result[x] = column;
        }

        return result;
    }

    static int SumOfBinomialCoefficients(int n, int k)
    {
        int result = 0;
        for (int i = 0; i <= k; i++)
            result += BinomialCoefficient(n, i);
        return result;
    }

    static int BinomialCoefficient(int n, int k)
    {
        long result = 1;
        for (int i = n - k + 1; i <= n; i++)
            result *= i;
        for (int i = 2; i <= k; i++)
            result /= i;
        return (int)result;
    }

    static void PrintMatrix(int[,] matrix)
    {
        int width = matrix.GetLength(0);
        int height = matrix.GetLength(1);

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
                Console.Write(matrix[x, y]);
            Console.WriteLine();
        }

        Console.WriteLine("Score: {0}/{1} = {2}", width, height, (double)width / height);
    }
}


class LongSet
{
    private static readonly int[] primes =
    {
        17, 37, 67, 89, 113, 149, 191, 239, 307, 389, 487, 613, 769, 967, 1213, 1523, 1907,
        2389, 2999, 3761, 4703, 5879, 7349, 9187, 11489, 14369, 17971, 22469, 28087, 35111,
        43889, 54869, 68597, 85751, 107197, 133999, 167521, 209431, 261791, 327247, 409063,
        511333, 639167, 798961, 998717, 1248407, 1560511, 1950643, 2438309, 3047909,
        809891, 4762367, 5952959, 7441219, 9301529, 11626913, 14533661, 18167089, 22708867,
        28386089, 35482627, 44353297, 55441637, 69302071, 86627603, 108284507, 135355669,
        169194593, 211493263, 264366593, 330458263, 413072843, 516341057, 645426329,
        806782913, 1008478649, 1260598321
    };

    private int[] _buckets;
    private int[] _nextItemIndexes;
    private long[] _items;
    private int _count;
    private int _minCapacity;
    private int _maxCapacity;
    private int _currentCapacity;

    public LongSet()
    {
        Initialize(0, 0);
    }

    private int GetPrime(int capacity)
    {
        foreach (int prime in primes)
            if (prime >= capacity)
                return prime;

        return int.MaxValue;
    }

    public void Reset(int minCapacity, int maxCapacity)
    {
        if (maxCapacity > _maxCapacity)
            Initialize(minCapacity, maxCapacity);
        else
            ClearBuckets();
    }

    private void Initialize(int minCapacity, int maxCapacity)
    {
        _minCapacity = GetPrime(minCapacity);
        _maxCapacity = GetPrime(maxCapacity);
        _currentCapacity = _minCapacity;

        _buckets = new int[_maxCapacity];
        _nextItemIndexes = new int[_maxCapacity];
        _items = new long[_maxCapacity];
        _count = 0;
    }

    private void ClearBuckets()
    {
        Array.Clear(_buckets, 0, _currentCapacity);
        _count = 0;
        _currentCapacity = _minCapacity;
    }

    public bool Add(long value)
    {
        int bucket = (int)((ulong)value % (ulong)_currentCapacity);
        for (int i = _buckets[bucket] - 1; i >= 0; i = _nextItemIndexes[i])
            if (_items[i] == value)
                return false;

        if (_count == _currentCapacity)
        {
            Grow();
            bucket = (int)((ulong)value % (ulong)_currentCapacity);
        }

        int index = _count;
        _items[index] = value;
        _nextItemIndexes[index] = _buckets[bucket] - 1;
        _buckets[bucket] = index + 1;
        _count++;

        return true;
    }

    private void Grow()
    {
        Array.Clear(_buckets, 0, _currentCapacity);

        const int growthFactor = 8;
        int newCapacity = GetPrime(_currentCapacity * growthFactor);
        if (newCapacity > _maxCapacity)
            newCapacity = _maxCapacity;
        _currentCapacity = newCapacity;

        for (int i = 0; i < _count; i++)
        {
            int bucket = (int)((ulong)_items[i] % (ulong)newCapacity);
            _nextItemIndexes[i] = _buckets[bucket] - 1;
            _buckets[bucket] = i + 1;
        }
    }

    public bool Contains(long value)
    {
        int bucket = (int)((ulong)value % (ulong)_buckets.Length);
        for (int i = _buckets[bucket] - 1; i >= 0; i = _nextItemIndexes[i])
            if (_items[i] == value)
                return true;

        return false;
    }

    public IReadOnlyList<long> GetValues()
    {
        return new ArraySegment<long>(_items, 0, _count);
    }
}

File konfigurasi:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>

Dalam beberapa hal Anda tampaknya lebih pesimis daripada dioptimalkan. Satu-satunya hal yang benar-benar terlihat seperti optimasi adalah memungkinkan bit untuk berbenturan dengan menggunakan ulongdan membiarkan shift shift alih-alih menggunakan BigInteger.
Peter Taylor

@PeterTaylor Optimasi yang paling penting adalah dalam fungsi HasPropertyX. Fungsi kembali segera setelah tabrakan jumlah kolom pertama ditemukan (tidak seperti fungsi scoreLyndonWord Anda). Saya juga telah mengurutkan topeng kolom sedemikian rupa sehingga kami pertama kali memeriksa set kolom yang lebih mungkin bertabrakan. Dua optimisasi ini meningkatkan kinerja dengan urutan besarnya.
Suboptimus Prime

Meskipun perubahan kinerja sering mengejutkan, pada prinsipnya pembatalan awal tidak boleh memberikan lebih dari faktor 2, dan GetSumOfColumnsmenambahkan loop tambahan yang saya harapkan harganya lebih dari faktor 2. Penyortiran topeng terdengar menarik: mungkin Anda bisa edit jawaban untuk berbicara sedikit tentangnya? (Dan pada titik tertentu saya akan bereksperimen dengan cara alternatif untuk melakukan aborsi awal: alasan saya tidak bisa melakukannya adalah bahwa HashSet tidak mendukung iterasi dan modifikasi bersamaan, tapi saya punya ide untuk menghindari kebutuhan akan iterator) .
Peter Taylor,

2
@justhalf, menggunakan pendekatan Gray-esque untuk iterasi atas subset dari ukuran tetap adalah benar-benar berharga. Itu memungkinkan saya untuk menemukan 26/14 dalam waktu kurang dari 9 menit dan 34 di antaranya dalam dua jam, di mana saya dibatalkan. Saat ini sedang menguji untuk melihat apakah saya bisa mendapatkan 28/15 dalam waktu yang wajar.
Peter Taylor

1
@Lembik, saya menjelajahi 29/15 secara lengkap dalam 75,5 jam. 31/16 akan memakan waktu sekitar 3 kali lebih lama - jadi lebih dari seminggu. Kami berdua telah membuat beberapa optimisasi sejak saya mulai menjalankan tes 29/15 itu, jadi mungkin itu akan menjadi seminggu sekarang. Tidak ada yang menghentikan Anda dari mengkompilasi kode saya atau kode SuboptimusPrime dan menjalankannya sendiri jika Anda memiliki komputer yang dapat Anda tinggalkan selama itu.
Peter Taylor
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.