Aqui está uma solução que não depende de matemática complexa como as respostas de sdcvvc / Dimitris Andreou, não altera a matriz de entrada como caf e o Coronel Panic, e não usa o conjunto de bits de tamanho enorme como Chris Lercher, JeremyP e muitos outros fizeram. Basicamente, comecei com a ideia de Svalorzen / Gilad Deutch para o Q2, generalizei para o caso comum Qk e implementei em Java para provar que o algoritmo funciona.
A ideia
Suponha que tenhamos um intervalo arbitrário I, do qual sabemos apenas que ele contém pelo menos um dos números ausentes. Após uma passagem através do feixe de entrada, olhando apenas para os números de Eu , que pode obter tanto a soma S e a quantidade Q de números em falta a partir de I . Fazemos isso simplesmente diminuindo o comprimento de I toda vez que encontramos um número de I (para obter Q ) e diminuindo a soma pré-calculada de todos os números em I pelo número encontrado toda vez (para obter S ).
Agora vamos olhar para S e Q . Se Q = 1 , isto significa que, em seguida, que contém apenas um dos números que faltam, e este número é claramente S . Marcamos I como concluído (é chamado de "inequívoco" no programa) e o deixamos de fora de uma análise mais aprofundada. Por outro lado, se Q> 1 , podemos calcular a média A = S / Q de números que faltam contida no I . Como todos os números são distintas, pelo menos uma de tais números é estritamente inferior a um e pelo menos um é estritamente maior que um . Agora dividimos eu em Aem dois intervalos menores, cada um dos quais contém pelo menos um número ausente. Observe que não importa a qual dos intervalos atribuímos A , caso seja um número inteiro.
Fazemos a próxima matriz passar calculando S e Q para cada um dos intervalos separadamente (mas na mesma passagem) e depois marcar intervalos com Q = 1 e dividir intervalos com Q> 1 . Continuamos esse processo até que não haja novos intervalos "ambíguos", ou seja, não temos nada para dividir porque cada intervalo contém exatamente um número ausente (e sempre o conhecemos porque conhecemos S ). Começamos a partir do único intervalo "todo o intervalo", contendo todos os números possíveis (como [1..N] na pergunta).
Análise de complexidade de tempo e espaço
O número total de passes p que precisamos fazer até o processo parar nunca é maior do que os números ausentes contam k . A desigualdade p <= k pode ser comprovada rigorosamente. Por outro lado, também existe um limite superior empírico p <log 2 N + 3 que é útil para grandes valores de k . Precisamos fazer uma pesquisa binária para cada número da matriz de entrada para determinar o intervalo ao qual ela pertence. Isso adiciona o multiplicador de log k à complexidade do tempo.
No total, a complexidade do tempo é O (N ᛫ min (k, log N) ᛫ log k) . Observe que, para k grande , isso é significativamente melhor que o método do sdcvvc / Dimitris Andreou, que é O (N ᛫ k) .
Por seu trabalho, o algoritmo requer O (k) espaço adicional para armazenar na maioria dos intervalos de k , o que é significativamente melhor que O (N) em soluções de "bitset".
Implementação Java
Aqui está uma classe Java que implementa o algoritmo acima. Ele sempre retorna uma matriz classificada de números ausentes. Além disso, ele não requer que os números ausentes contem k, porque o calcula na primeira passagem. Todo o intervalo de números é dado pelos parâmetros minNumber
e maxNumber
(por exemplo, 1 e 100 para o primeiro exemplo da pergunta).
public class MissingNumbers {
private static class Interval {
boolean ambiguous = true;
final int begin;
int quantity;
long sum;
Interval(int begin, int end) { // begin inclusive, end exclusive
this.begin = begin;
quantity = end - begin;
sum = quantity * ((long)end - 1 + begin) / 2;
}
void exclude(int x) {
quantity--;
sum -= x;
}
}
public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
Interval full = new Interval(minNumber, ++maxNumber);
for (inputBag.startOver(); inputBag.hasNext();)
full.exclude(inputBag.next());
int missingCount = full.quantity;
if (missingCount == 0)
return new int[0];
Interval[] intervals = new Interval[missingCount];
intervals[0] = full;
int[] dividers = new int[missingCount];
dividers[0] = minNumber;
int intervalCount = 1;
while (true) {
int oldCount = intervalCount;
for (int i = 0; i < oldCount; i++) {
Interval itv = intervals[i];
if (itv.ambiguous)
if (itv.quantity == 1) // number inside itv uniquely identified
itv.ambiguous = false;
else
intervalCount++; // itv will be split into two intervals
}
if (oldCount == intervalCount)
break;
int newIndex = intervalCount - 1;
int end = maxNumber;
for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
// newIndex always >= oldIndex
Interval itv = intervals[oldIndex];
int begin = itv.begin;
if (itv.ambiguous) {
// split interval itv
// use floorDiv instead of / because input numbers can be negative
int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
intervals[newIndex--] = new Interval(mean, end);
intervals[newIndex--] = new Interval(begin, mean);
} else
intervals[newIndex--] = itv;
end = begin;
}
for (int i = 0; i < intervalCount; i++)
dividers[i] = intervals[i].begin;
for (inputBag.startOver(); inputBag.hasNext();) {
int x = inputBag.next();
// find the interval to which x belongs
int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
if (i < 0)
i = -i - 2;
Interval itv = intervals[i];
if (itv.ambiguous)
itv.exclude(x);
}
}
assert intervalCount == missingCount;
for (int i = 0; i < intervalCount; i++)
dividers[i] = (int)intervals[i].sum;
return dividers;
}
}
Para ser justo, essa classe recebe entrada na forma de NumberBag
objetos. NumberBag
não permite modificação de matriz e acesso aleatório e também conta quantas vezes a matriz foi solicitada para deslocamento sequencial. Também é mais adequado para testes de grandes matrizes do que Iterable<Integer>
porque evita o encaixe de int
valores primitivos e permite envolver uma parte de um grande int[]
para uma preparação de teste conveniente. Não é difícil substituir, se desejado, NumberBag
por int[]
ou Iterable<Integer>
digitar a find
assinatura, alterando dois loops for para lo em foreach.
import java.util.*;
public abstract class NumberBag {
private int passCount;
public void startOver() {
passCount++;
}
public final int getPassCount() {
return passCount;
}
public abstract boolean hasNext();
public abstract int next();
// A lightweight version of Iterable<Integer> to avoid boxing of int
public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
return new NumberBag() {
int index = toIndex;
public void startOver() {
super.startOver();
index = fromIndex;
}
public boolean hasNext() {
return index < toIndex;
}
public int next() {
if (index >= toIndex)
throw new NoSuchElementException();
return base[index++];
}
};
}
public static NumberBag fromArray(int[] base) {
return fromArray(base, 0, base.length);
}
public static NumberBag fromIterable(Iterable<Integer> base) {
return new NumberBag() {
Iterator<Integer> it;
public void startOver() {
super.startOver();
it = base.iterator();
}
public boolean hasNext() {
return it.hasNext();
}
public int next() {
return it.next();
}
};
}
}
Testes
Exemplos simples que demonstram o uso dessas classes são dados abaixo.
import java.util.*;
public class SimpleTest {
public static void main(String[] args) {
int[] input = { 7, 1, 4, 9, 6, 2 };
NumberBag bag = NumberBag.fromArray(input);
int[] output = MissingNumbers.find(1, 10, bag);
System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
Arrays.toString(input), Arrays.toString(output), bag.getPassCount());
List<Integer> inputList = new ArrayList<>();
for (int i = 0; i < 10; i++)
inputList.add(2 * i);
Collections.shuffle(inputList);
bag = NumberBag.fromIterable(inputList);
output = MissingNumbers.find(0, 19, bag);
System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
inputList, Arrays.toString(output), bag.getPassCount());
// Sieve of Eratosthenes
final int MAXN = 1_000;
List<Integer> nonPrimes = new ArrayList<>();
nonPrimes.add(1);
int[] primes;
int lastPrimeIndex = 0;
while (true) {
primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
int p = primes[lastPrimeIndex]; // guaranteed to be prime
int q = p;
for (int i = lastPrimeIndex++; i < primes.length; i++) {
q = primes[i]; // not necessarily prime
int pq = p * q;
if (pq > MAXN)
break;
nonPrimes.add(pq);
}
if (q == p)
break;
}
System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
primes.length, MAXN);
for (int i = 0; i < primes.length; i++)
System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
}
}
O teste de grandes matrizes pode ser realizado desta maneira:
import java.util.*;
public class BatchTest {
private static final Random rand = new Random();
public static int MIN_NUMBER = 1;
private final int minNumber = MIN_NUMBER;
private final int numberCount;
private final int[] numbers;
private int missingCount;
public long finderTime;
public BatchTest(int numberCount) {
this.numberCount = numberCount;
numbers = new int[numberCount];
for (int i = 0; i < numberCount; i++)
numbers[i] = minNumber + i;
}
private int passBound() {
int mBound = missingCount > 0 ? missingCount : 1;
int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
return Math.min(mBound, nBound);
}
private void error(String cause) {
throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
}
// returns the number of times the input array was traversed in this test
public int makeTest(int missingCount) {
this.missingCount = missingCount;
// numbers array is reused when numberCount stays the same,
// just Fisher–Yates shuffle it for each test
for (int i = numberCount - 1; i > 0; i--) {
int j = rand.nextInt(i + 1);
if (i != j) {
int t = numbers[i];
numbers[i] = numbers[j];
numbers[j] = t;
}
}
final int bagSize = numberCount - missingCount;
NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
finderTime -= System.nanoTime();
int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
finderTime += System.nanoTime();
if (inputBag.getPassCount() > passBound())
error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
if (found.length != missingCount)
error("wrong result length");
int j = bagSize; // "missing" part beginning in numbers
Arrays.sort(numbers, bagSize, numberCount);
for (int i = 0; i < missingCount; i++)
if (found[i] != numbers[j++])
error("wrong result array, " + i + "-th element differs");
return inputBag.getPassCount();
}
public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
BatchTest t = new BatchTest(numberCount);
System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
int minPass = Integer.MAX_VALUE;
int passSum = 0;
int maxPass = 0;
t.finderTime = 0;
for (int j = 1; j <= repeats; j++) {
int pCount = t.makeTest(missingCount);
if (pCount < minPass)
minPass = pCount;
passSum += pCount;
if (pCount > maxPass)
maxPass = pCount;
}
System.out.format("║ %9d %9d ║ %2d %5.2f %2d ║ %11.3f ║%n", missingCount, numberCount, minPass,
(double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
}
}
public static void main(String[] args) {
System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
System.out.println("║ Number count ║ Passes ║ Average time ║");
System.out.println("║ missimg total ║ min avg max ║ per search (ms) ║");
long time = System.nanoTime();
strideCheck(100, 0, 100, 1, 20_000);
strideCheck(100_000, 2, 99_998, 1_282, 15);
MIN_NUMBER = -2_000_000_000;
strideCheck(300_000_000, 1, 10, 1, 1);
time = System.nanoTime() - time;
System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
}
}
Experimente-os no Ideone