Conheça uma sequência por suas subsequências

18

Introdução

Suponha que você e seu amigo estejam jogando um jogo. Seu amigo pensa em uma sequência específica de nbits e sua tarefa é deduzir a sequência fazendo perguntas. No entanto, o único tipo de pergunta que você pode fazer é "Quanto tempo é a subsequência comum mais longa da sua sequência e S", onde Sestá qualquer sequência de bits. Quanto menos perguntas você precisar, melhor.

A tarefa

Sua tarefa é escrever um programa ou função que tenha como entrada um número inteiro positivo ne uma sequência binária Rde comprimento n. A sequência pode ser uma matriz de números inteiros, uma sequência ou outro tipo razoável de sua escolha. Seu programa deve produzir a sequência R.

Seu programa não tem permissão para acessar a sequência Rdiretamente. A única coisa que ele pode fazer Ré fornecê-lo como entrada para a função, len_lcsjuntamente com outra sequência binária S. A função len_lcs(R, S)retorna o comprimento da subsequência comum mais longa de Re S. Isso significa a sequência mais longa de bits que ocorre como uma subsequência (não necessariamente contígua) em ambos Re S. As entradas len_lcspodem ter diferentes comprimentos. O programa deve chamar essa função Re outras seqüências várias vezes e, em seguida, reconstruir a sequência com Rbase nessas informações.

Exemplo

Considere as entradas n = 4e R = "1010". Primeiro, podemos avaliar len_lcs(R, "110"), o que fornece 3, uma vez que "110"é a subsequência comum mais longa de "1010"e "110". Então sabemos que Ré obtido "110"inserindo um bit em alguma posição. Em seguida, podemos tentar len_lcs(R, "0110"), que retorna 3desde que as subsequências comuns mais longas são "110"e "010", portanto, "0110"não estão corretas. Então tentamos len_lcs(R, "1010"), que retorna 4. Agora sabemos disso R == "1010", para que possamos retornar essa sequência como a saída correta. Isso exigiu 3 chamadas para len_lcs.

Regras e pontuação

Em este repositório , você encontrará um arquivo chamado subsequence_data.txtcontendo 100 sequências binárias aleatórias de comprimentos entre 75 e 124. Eles foram gerados por tomar três carros alegóricos aleatórios entre 0 e 1, tendo sua média como a, e em seguida, lançando uma amoeda -biased nvezes. Você pontua é o número médio de chamadas paralen_lcs essas seqüências, sendo a pontuação mais baixa melhor. Sua submissão deve registrar o número de chamadas. Não há limites de tempo, exceto que você deve executar seu programa no arquivo antes de enviá-lo.

Sua submissão deve ser determinística. Os PRNGs são permitidos, mas devem usar a data de hoje 200116(ou o equivalente mais próximo) como a semente aleatória. Você não tem permissão para otimizar seu envio em relação a esses casos de teste específicos. Se eu suspeitar que isso esteja acontecendo, gerarei um novo lote.

Como não é um código de golfe, você deve escrever um código legível. O Código Rosetta possui uma página na subsequência comum mais longa ; você pode usá-lo para implementar len_lcsno seu idioma preferido.

Zgarb
fonte
Idéia legal! Isso tem algum aplicativo?
flawr
@ Flawr Não conheço nenhuma aplicação direta. A idéia surgiu da teoria da complexidade das consultas , um subcampo da ciência da computação, e que possui muitas aplicações.
Zgarb
Eu acho que seria ótimo ter o mesmo desafio novamente, mas onde você pode acessar em lcsvez de len_lcs.
flawr
@ flawr Isso não seria muito interessante, já que lcs(R, "01"*2*n)retorna R. ;) Mas isso poderia funcionar se chamada lcs(R, S)iria aumentar a pontuação len(S)em vez de 1, ou algo assim ...
Zgarb
1
Eu adoraria ver outras respostas = S
flawr 24/01

Respostas:

10

Java, 99.04 98.46 97.66 chamadas lcs ()

Como funciona

Exemplo: Nossa linha que deve ser reconstruída é 00101. Primeiro, descobrimos quantos zeros existem, comparando (aqui comparando = computando lcs com a cadeia original) por uma cadeia apenas de zeros 00000. Em seguida, passamos por cada posição, vire 0para a 1e verifique se agora temos uma substring comum mais longa. Se sim, aceite e vá para a próxima posição; se não, 1volte a corrente para a 0e vá para a próxima posição:

For our example of "00101" we get following steps:
input  lcs  prev.'best'
00000  3    0           //number of zeros
̲10000  3    3           //reject
0̲1000  3    3           //reject
00̲100  4    3           //accept
001̲10  4    4           //reject
0010̲1  5    4           //accept

Otimizações

Esta é apenas uma implementação "ingênua"; talvez seja possível encontrar um alogritmo mais sofisticado que verifique várias posições ao mesmo tempo. Mas não tenho certeza se realmente existe um melhor (por exemplo, com base no cálculo de bits de paridade semelhantes ao código de Hamming), pois você sempre pode avaliar o comprimento da substring comum.

Para uma determinada linha de dígitos, esse algoritmo precisa exatamente de #ofDigitsUntilTheLastOccurenceOf1 + 1verificações. (Subtraia um se o último dígito for um 1.)

EDIT: Uma pequena otimização: se apenas verificarmos o segundo último dígito e ainda precisarmos inserir um 1, sabemos com certeza que ele deve estar na última posição e podemos omitir a verificação correspondente.

EDIT2: Acabei de notar que você pode aplicar a ideia acima às últimas k.

Obviamente, pode ser possível obter uma pontuação um pouco menor com essa otimização, reordenando todas as linhas primeiro, porque pode ser que você obtenha mais linhas com as linhas no final, mas isso obviamente seria uma otimização para a atual casos de teste que não são mais engraçados.

Tempo de execução

O limite superior é O(#NumberOfBits).

Código completo

Aqui está o código completo:

package jcodegolf;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

// http://codegolf.stackexchange.com/questions/69799/know-a-sequence-by-its-subsequences

public class SequenceReconstructor { 
    public static int counter = 0;
    public static int lcs(String a, String b) { //stolen from http://rosettacode.org/wiki/Longest_common_subsequence#Java
        int[][] lengths = new int[a.length()+1][b.length()+1];

        // row 0 and column 0 are initialized to 0 already

        for (int i = 0; i < a.length(); i++)
            for (int j = 0; j < b.length(); j++)
                if (a.charAt(i) == b.charAt(j))
                    lengths[i+1][j+1] = lengths[i][j] + 1;
                else
                    lengths[i+1][j+1] =
                        Math.max(lengths[i+1][j], lengths[i][j+1]);

        // read the substring out from the matrix
        StringBuffer sb = new StringBuffer();
        for (int x = a.length(), y = b.length();
             x != 0 && y != 0; ) {
            if (lengths[x][y] == lengths[x-1][y])
                x--;
            else if (lengths[x][y] == lengths[x][y-1])
                y--;
            else {
                assert a.charAt(x-1) == b.charAt(y-1);
                sb.append(a.charAt(x-1));
                x--;
                y--;
            }
        }

        counter ++;
        return sb.reverse().toString().length();
    }


    public static String reconstruct(String secretLine, int lineLength){
        int current_lcs = 0; 
        int previous_lcs = 0;
        char [] myGuess = new char[lineLength];
        for (int k=0; k<lineLength; k++){
            myGuess[k] = '0';
        }

        //find the number of zeros:
        int numberOfZeros = lcs(secretLine, String.valueOf(myGuess));
        current_lcs = numberOfZeros;
        previous_lcs = numberOfZeros;

        if(current_lcs == lineLength){ //were done
            return String.valueOf(myGuess);
        }


        int numberOfOnes = lineLength - numberOfZeros;
        //try to greedily insert ones at the positions where they maximize the common substring length
        int onesCounter = 0;
        for(int n=0; n < lineLength && onesCounter < numberOfOnes; n++){

            myGuess[n] = '1';
            current_lcs = lcs(secretLine, String.valueOf(myGuess));

            if(current_lcs > previous_lcs){ //accept

                previous_lcs = current_lcs;
                onesCounter ++;

            } else { // do not accept
                myGuess[n]='0';     
            }

            if(n == lineLength-(numberOfOnes-onesCounter)-1 && onesCounter < numberOfOnes){ //lets test if we have as many locations left as we have ones to insert
                                                                // then we know that the rest are ones
                for(int k=n+1;k<lineLength;k++){
                    myGuess[k] = '1';
                }
                break;
            }

        }

        return String.valueOf(myGuess);
    }

    public static void main(String[] args) {
        try {

            //read the file
            BufferedReader br;

            br = new BufferedReader(new FileReader("PATH/TO/YOUR/FILE/LOCATION/subsequence_data.txt"));

            String line;

            //iterate over each line
            while ( (line = br.readLine()) != null){

                String r = reconstruct(line, line.length());
                System.out.println(line);     //print original line
                System.out.println(r);        //print current line
                System.out.println(counter/100.0);  //print current number of calls
                if (! line.equals(r)){
                    System.out.println("SOMETHING WENT HORRIBLY WRONG!!!");
                    System.exit(1);
                }

            }


        } catch(Exception e){
            e.printStackTrace();;
        }

    }

}
flawr
fonte
1
Como você recebe menos chamadas quando há 1s à direita, parece que você poderia fazer melhor, em média, se, após o primeiro palpite informar que há mais 0s que 1s, você passa a procurar 0 posições em vez de 1 posições. Você pode fazer isso várias vezes.
histocrata
1
@histocrat Eu acho que ele já está parando uma vez que ele usa o último 1, o que equivale a haver apenas zeros.
Martin Ender