Una sfida di ottimizzazione con strane monete


17

Hai nmonete che pesano ciascuna -1 o 1. Ognuna è etichettata da 0a in n-1modo da poter distinguere le monete. Hai anche un dispositivo di pesatura (magico). Al primo giro puoi mettere tutte le monete che vuoi sul dispositivo di pesatura che è in grado di misurare pesi sia negativi che positivi e ti dirà esattamente quanto pesano.

Tuttavia, c'è qualcosa di veramente strano nel dispositivo di pesatura. Se metti le monete x_1, x_2, ..., x_jsul dispositivo per la prima volta, la prossima volta devi mettere le monete (x_1+1), (x_2+1) , ..., (x_j+1)sulla bilancia con l'eccezione che ovviamente non puoi mettere su una moneta con un numero maggiore di n-1. Non solo, per ogni nuova pesatura puoi scegliere se vuoi anche mettere la moneta 0sulla bilancia.

In base a questa regola, qual è il numero più piccolo di pesature che ti dirà sempre esattamente quali monete pesano 1 e quali pesano -1?

Chiaramente potresti semplicemente mettere una moneta 0sul dispositivo nel primo turno e poi ci vorrebbero esattamente npesate per risolvere il problema.

Lingue e biblioteche

Puoi usare qualsiasi lingua o libreria che ti piace (che non è stata progettata per questa sfida). Tuttavia, vorrei essere in grado di testare il tuo codice, se possibile, quindi se puoi fornire istruzioni chiare su come eseguirlo in Ubuntu sarebbe molto apprezzato.

Punto

Per un dato momento nil punteggio è ndiviso per il numero di pesate necessarie nel caso peggiore. I punteggi più alti sono quindi migliori. Non ci sono input per questo puzzle, ma il tuo obiettivo è quello di trovare un nper cui puoi ottenere il punteggio più alto.

Se c'è un pareggio, vince la prima risposta. Nella situazione estremamente improbabile in cui qualcuno trova un modo per ottenere un punteggio infinito, quella persona vince immediatamente.

Compito

Il tuo compito è semplicemente quello di scrivere il codice che ottiene il punteggio più alto. Il tuo codice dovrà scegliere una n in modo intelligente e quindi ottimizzare anche il numero di pesate n.

Voci principali

  • 4/3 7/5 in Python di Sarge Borsch
  • 26/14 in Java da Peter Taylor

8
Mi piacerebbe mettere le mani su alcune monete anti-gravità.
mbomb007,

2
Ho una soluzione che non usa mai la macchina: afferrare ogni moneta e vedere quali tirano su la mano e quali abbassano la mano.
Finanzi la causa di Monica il

1
Inoltre, come nota a margine, potrebbe essere meglio scrivere "se pesate le monete da a b, quindi la prossima volta che dovrete fare da + 1 a b + 1" (magari con un 'almeno' gettato anche dentro, e una migliore formattazione) anziché gli indici che indicano il numero della moneta. Ciò fa sembrare che sia una proprietà o una quantità di moneta _, invece della moneta stessa.
Finanzi la causa di Monica il

1
@ mbomb007 Ad ogni pesata puoi scegliere di pesare la moneta 0 e tutte le altre monete che peserai. In altre parole, hai una nuova scelta da fare per ogni pesatura che fai.

3
@ mbomb007 @QPaysTaxes Per quanto riguarda la notazione x_i: possiamo avere ad esempio una prima pesata di (x_1, x_2, x_3) = (3, 2, 7), e quindi la seconda pesatura può essere (4, 3, 8) o ( 0, 4, 3, 8). Le etichette delle monete non devono necessariamente essere consecutive e l'indice iin x_inon si riferisce all'etichetta della moneta.
Mitch Schwartz,

Risposte:


3

C ++, punteggio 23/12 25/13 27/14 28/14 = 2 31/15

Le soluzioni della proprietà Matrix X rivisitate (o Joy of X) sono direttamente utilizzabili come soluzioni a questo problema. Ad esempio la soluzione di 31 righe 15 colonne:

1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 0 1 1 0 
1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 0 1 1 
1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 0 1 
1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 0 
1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 1 
0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 1 
0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 1 
1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 1 
0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 0 
0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 0 
0 0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 0 
1 0 0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 1 
0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 0 
0 0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 0 
1 0 0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 

la riga N rappresenta le monete che hai messo sulla bilancia per la misurazione N. Qualunque sia il risultato della ponderazione che ottieni, c'è ovviamente un insieme di valori di monete che danno quel peso. Se esiste anche un'altra combinazione (la soluzione non è unica) considera come differiscono. È necessario sostituire un insieme di ponderazione delle monete con la ponderazione delle 1monete -1. Questo dà una serie di colonne che corrispondono a quella vibrazione. Esiste anche un insieme di ponderazione delle monete -1da sostituire 1. Questo è un altro insieme di colonne. Poiché le misure non cambiano tra le due soluzioni, ciò significa che le somme delle colonne dei due insiemi devono essere le stesse. Ma le soluzioni alla proprietà Matrix X rivisitate (o la gioia di X) sono esattamente queste matrici in cui non esistono tali set di colonne, quindi non ci sono duplicati e ogni soluzione è unica.

Ogni serie effettiva di misure può essere descritta da una 0/1matrice. Ma anche se alcuni insiemi di colonne si sommano agli stessi vettori, è possibile che i segni dei valori delle monete della soluzione candidata non corrispondano esattamente a tale insieme. Quindi non so se matrici come quella sopra siano ottimali. Ma almeno forniscono un limite inferiore. Quindi la possibilità che 31 monete possano essere fatte in meno di 15 misurazioni è ancora aperta.

Si noti che ciò è vero solo per una strategia non fissa in cui la decisione di mettere la moneta 0sulla bilancia dipende dal risultato delle ponderazioni precedenti. In caso contrario, si dovrà avere soluzioni in cui i segni delle monete corrispondono con i set che hanno la stessa somma di colonna.


Il record del mondo attuale :)

Quanto velocemente un computer stima sarebbe necessario per arrivare a 2?

@Lembik Non sono convinto che 2 sia possibile. Non so perché, ma i risultati attuali suggeriscono che puoi avvicinarti a 2 solo arbitrariamente senza mai raggiungerlo
Ton Hospel

Hai avuto la possibilità di verificare la matrice circolante 25 per 50 che ho incollato che dovrebbe dare 2? 01011011100010111101000001100111110011010100011010 come prima riga di una matrice circolante.

Non so nemmeno controllare quella matrice senza scrivere un programma dedicato che durerà a lungo
Ton Hospel

5

Python 2, punteggio = 1.0

Questo è il punteggio facile, nel caso in cui nessuno trovi un punteggio migliore (dubbio). npesate per ciascuno n.

import antigravity
import random

def weigh(coins, indices):
    return sum(coins[i] for i in indices)

def main(n):
    coins = [random.choice([-1,1]) for i in range(n)]
    for i in range(len(coins)):
        print weigh(coins, [i]),

main(4)

Ho importato in antigravitymodo che il programma possa funzionare con pesi negativi.


Molto utile. Grazie :)

L'importazione antigravityè sostanzialmente no-op, giusto?
Visualizza nome

@SargeBorsch Ai fini di questo programma, lo è. Ma in realtà fa qualcosa.
mbomb007,

5

Punteggio = 26/14 ~ = 1.857

import java.util.*;

public class LembikWeighingOptimisation {

    public static void main(String[] args) {
        float best = 0;
        int opt = 1;
        for (int n = 6; n < 32; n+=2) {
            long start = System.nanoTime();
            System.out.format("%d\t", n);
            opt = optimise(n, n / 2 + 1);
            float score = n / (float)opt;
            System.out.format("%d\t%f", opt, score);
            if (score > best) {
                best = score;
                System.out.print('*');
            }
            System.out.format(" in %d seconds", (System.nanoTime() - start) / 1000000000);
            System.out.println();
        }
    }

    private static int optimise(int numCoins, int minN) {
        MaskRange.N = numCoins;
        Set<MaskRange> coinSets = new HashSet<MaskRange>();
        coinSets.add(new MaskRange(0, 0));

        int allCoins = (1 << numCoins) - 1;

        for (int n = minN; n < numCoins; n++) {
            for (int startCoins = 1; startCoins * 2 <= numCoins; startCoins++) {
                for (int mask = (1 << startCoins) - 1; mask < (1 << numCoins); ) {
                    // Quick-reject: in n turns, do we cover the entire set?
                    int qr = (1 << (n-1)) - 1;
                    for (int j = 0; j < n; j++) qr |= mask << j;
                    if ((qr & allCoins) == allCoins && canDistinguishInNTurns(mask, coinSets, n)) {
                        System.out.print("[" + Integer.toBinaryString(mask) + "] ");
                        return n;
                    }

                    // Gosper's hack to update
                    int c = mask & -mask;
                    int r = mask + c;
                    mask = (((r^mask) >>> 2) / c) | r;
                }
            }
        }

        return numCoins;
    }

    private static boolean canDistinguishInNTurns(int mask, Set<MaskRange> coinsets, int n) {
        if (n < 0) throw new IllegalArgumentException("n");
        int count = 0;
        for (MaskRange mr : coinsets) count += mr.size();
        if (count <= 1) return true;
        if (n == 0) return false;

        // Partition.
        Set<MaskRange>[] p = new Set[Integer.bitCount(mask) + 1];
        for (int i = 0; i < p.length; i++) p[i] = new HashSet<MaskRange>();
        for (MaskRange range : coinsets) range.partition(mask, p);

        for (int d = 0; d < 2; d++) {
            boolean ok = true;
            for (Set<MaskRange> s : p) {
                if (!canDistinguishInNTurns((mask << 1) + d, s, n - 1)) {
                    ok = false;
                    break;
                }
            }

            if (ok) return true;
        }

        return false;
    }

    static class MaskRange {
        public static int N;
        public final int mask, value;

        public MaskRange(int mask, int value) {
            this.mask = mask;
            this.value = value & mask;
            if (this.value != value) throw new IllegalArgumentException();
        }

        public int size() {
            return 1 << (N - Integer.bitCount(mask));
        }

        public void partition(int otherMask, Set<MaskRange>[] p) {
            otherMask &= (1 << N) - 1;

            int baseline = Integer.bitCount(value & otherMask);
            int variables = otherMask & ~mask;
            int union = mask | otherMask;
            partitionInner(value, union, variables, baseline, p);
        }

        private static void partitionInner(int v, int m, int var, int baseline, Set<MaskRange>[] p) {
            if (var == 0) {
                p[baseline].add(new MaskRange(m, v));
            }
            else {
                int lowest = var & (1 + ~var);
                partitionInner(v,          m, var & ~lowest, baseline, p);
                partitionInner(v | lowest, m, var & ~lowest, baseline + 1, p);
            }
        }

        @Override
        public String toString() {
            return String.format("(x & %x = %x)", mask, value);
        }
    }
}

Salva come LembikWeighingOptimisation.java, compila come javac LembikWeighingOptimisation.java, esegui come java LembikWeighingOptimisation.

Mille grazie a Mitch Schwartz per aver segnalato un bug nella prima versione del rifiuto rapido.

Questo utilizza alcune tecniche abbastanza basilari che non posso giustificare rigorosamente. Forza bruta, ma solo per iniziare operazioni di pesatura che utilizzano al massimo metà delle monete: le sequenze che utilizzano più della metà delle monete non sono direttamente trasferibili alle pesate complementari (perché non conosciamo il peso totale), ma a un livello ondulato a mano ci dovrebbe essere circa la stessa quantità di informazioni. Esegue inoltre le iterazioni iniziali in base al numero di monete coinvolte, sulla base del fatto che in questo modo copre le pesature disperse (che si spera forniscano informazioni sull'estremità superiore relativamente presto) senza prima strisciare attraverso un mazzo che inizia con un sottoinsieme denso in l'estremità inferiore.

La MaskRangeclasse è un enorme miglioramento rispetto alla versione precedente in termini di utilizzo della memoria e rimuove GC da un collo di bottiglia.

20      [11101001010] 11        1.818182* in 5364 seconds
22      [110110101000] 12       1.833333* in 33116 seconds
24      [1000011001001] 13      1.846154* in 12181 seconds                                                                                                            
26      [100101001100000] 14    1.857143* in 73890 seconds  

Non puoi assolutamente ottenere 12/7? Sono abbastanza sicuro che funzioni. Inoltre, che ne dici del 19/10? Pensavo che il mio codice me lo avesse dato una volta, ma ora non riesco a riprodurlo.

@Lembik, ho elencato 12/7, ma il meglio che posso fare per 19 è il 19/11.
Peter Taylor,

Oh si scusa. È possibile che il tuo euristico abbia eliminato alcune soluzioni? Sono abbastanza sicuro che anche il 19/10 dovrebbe funzionare.

È possibile , sì, se l'unica soluzione ha una pesatura iniziale con più della metà delle monete. Sarei leggermente sorpreso, però.
Peter Taylor,

Vale la pena aumentare la mezza soglia a un po 'più della metà, forse solo per vedere?

2

Python 3, punteggio = 4/3 = 1,33… (N = 4) punteggio = 1,4 (N = 7)

Aggiornamento: implementata la ricerca della forza bruta nel set di solutori "statici" e ottenuto un nuovo risultato

Penso che possa essere ulteriormente migliorato cercando solutori dinamici, che possono utilizzare i risultati di ponderazione per ulteriori decisioni.

Ecco un codice Python che cerca tutti i solutori statici alla ricerca di piccoli n valori (questi solutori pesano sempre gli stessi set di monete, da cui il nome "statico") e determina il loro numero peggiore di passi controllando semplicemente che i loro risultati di misurazione consentano solo una moneta corrispondente impostato in tutti i casi. Inoltre, tiene traccia del miglior punteggio trovato finora e dei primi risolutori di prugne che avevano dimostrato che sono decisamente peggiori di quelli che erano stati trovati prima. Questa è stata un'importante ottimizzazione, altrimenti non avrei potuto aspettare questo risultato conn = 7. (Ma chiaramente non è ancora ottimizzato molto bene)

Sentiti libero di fare domande se non è chiaro come funziona ...

#!/usr/bin/env python3
import itertools
from functools import partial


def get_all_possible_coinsets(n):
    return tuple(itertools.product(*itertools.repeat((-1, 1), n)))


def weigh(coinset, indexes_to_weigh):
    return sum(coinset[x] for x in indexes_to_weigh)


# made_measurements: [(indexes, weight)]
def filter_by_measurements(coinsets, made_measurements):
    return filter(lambda cs: all(w == weigh(cs, indexes) for indexes, w in made_measurements), coinsets)


class Position(object):
    def __init__(self, all_coinsets, coinset, made_measurements=()):
        self.all_coinsets = all_coinsets
        self.made_measurements = made_measurements
        self.coins = coinset

    def possible_coinsets(self):
        return tuple(filter_by_measurements(self.all_coinsets, self.made_measurements))

    def is_final(self):
        possible_coinsets = self.possible_coinsets()
        return (len(possible_coinsets) == 1) and possible_coinsets[0] == self.coins

    def move(self, measurement_indexes):
        measure_result = (measurement_indexes, weigh(self.coins, measurement_indexes))
        return Position(self.all_coinsets, self.coins, self.made_measurements + (measure_result,))


def get_all_start_positions(coinsets):
    for cs in coinsets:
        yield Position(coinsets, cs)


def average(xs):
    return sum(xs) / len(xs)


class StaticSolver(object):
    def __init__(self, measurements):
        self.measurements = measurements

    def choose_move(self, position: Position):
        index = len(position.made_measurements)
        return self.measurements[index]

    def __str__(self, *args, **kwargs):
        return 'StaticSolver({})'.format(', '.join(map(lambda x: '{' + ','.join(map(str, x)) + '}', self.measurements)))

    def __repr__(self):
        return str(self)


class FailedSolver(Exception):
    pass


def test_solvers(solvers, start_positions, max_steps):
    for solver in solvers:
        try:
            test_results = tuple(map(partial(test_solver, solver=solver, max_steps=max_steps), start_positions))
            yield (solver, max(test_results))
        except FailedSolver:
            continue


def all_measurement_starts(n):
    for i in range(1, n + 1):
        yield from itertools.combinations(range(n), i)


def next_measurement(n, measurement, include_zero):
    shifted = filter(lambda x: x < n, map(lambda x: x + 1, measurement))
    if include_zero:
        return tuple(itertools.chain((0,), shifted))
    else:
        return tuple(shifted)


def make_measurement_sequence(n, start, zero_decisions):
    yield start
    m = start
    for zero_decision in zero_decisions:
        m = next_measurement(n, m, zero_decision)
        yield m


def measurement_sequences_from_start(n, start, max_steps):
    continuations = itertools.product(*itertools.repeat((True, False), max_steps - 1))
    for c in continuations:
        yield tuple(make_measurement_sequence(n, start, c))


def all_measurement_sequences(n, max_steps):
    starts = all_measurement_starts(n)
    for start in starts:
        yield from measurement_sequences_from_start(n, start, max_steps)


def all_static_solvers(n, max_steps):
    return map(StaticSolver, all_measurement_sequences(n, max_steps))


def main():
    best_score = 1.0
    for n in range(1, 11):
        print('Searching with N = {}:'.format(n))
        coinsets = get_all_possible_coinsets(n)
        start_positions = tuple(get_all_start_positions(coinsets))


        # we are not interested in solvers with worst case number of steps bigger than this
        max_steps = int(n / best_score)

        solvers = all_static_solvers(n, max_steps)
        succeeded_solvers = test_solvers(solvers, start_positions, max_steps)

        try:
            best = min(succeeded_solvers, key=lambda x: x[1])
        except ValueError:  # no successful solvers
            continue
        score = n / best[1]
        best_score = max(score, best_score)
        print('{}, score = {}/{} = {}'.format(best, n, best[1], score))
    print('That\'s all!')


def test_solver(start_position: Position, solver, max_steps):
    p = start_position
    steps = 0
    try:
        while not p.is_final():
            steps += 1
            if steps > max_steps:
                raise FailedSolver
            p = p.move(solver.choose_move(p))
        return steps
    except IndexError:  # solution was not found after given steps — this solver failed to beat score 1
        raise FailedSolver


if __name__ == '__main__':
    main()

Il risultato:

Searching with N = 1:
(StaticSolver({0}), 1), score = 1/1 = 1.0
Searching with N = 2:
(StaticSolver({0}, {0,1}), 2), score = 2/2 = 1.0
Searching with N = 3:
(StaticSolver({0}, {0,1}, {0,1,2}), 3), score = 3/3 = 1.0
Searching with N = 4:
(StaticSolver({0,1}, {1,2}, {0,2,3}, {0,1,3}), 3), score = 4/3 = 1.3333333333333333
Searching with N = 5:
Searching with N = 6:
Searching with N = 7:
(StaticSolver({0,2}, {0,1,3}, {0,1,2,4}, {1,2,3,5}, {0,2,3,4,6}), 5), score = 7/5 = 1.4
Searching with N = 8:
Searching with N = 9:
(I gave up waiting at this moment)

Questa linea (StaticSolver({0,2}, {0,1,3}, {0,1,2,4}, {1,2,3,5}, {0,2,3,4,6}), 5), score = 7/5 = 1.4scopre il miglior risolutore trovato. I numeri tra {}parentesi graffe sono gli indici delle monete da mettere sul dispositivo di ponderazione ad ogni passo.


4
PS Ho scritto questo mentre la fonte di energia elettrica in casa mia era rotta, quindi avevo un laptop alimentato a batteria e nessuna connessione a Internet, e semplicemente non avevo cose migliori da fare che rompere alcuni enigmi. Immagino che non mi preoccuperei se tutto andava bene: D
Visualizza nome
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.