Ini adalah solusi yang tidak bergantung pada matematika kompleks seperti jawaban sdcvvc / Dimitris Andreou, tidak mengubah array input seperti yang dilakukan caf dan Kolonel Panic, dan tidak menggunakan bitset ukuran besar seperti Chris Lercher, JeremyP dan banyak yang melakukannya. Pada dasarnya, saya mulai dengan ide Svalorzen / Gilad Deutch untuk Q2, menggeneralisasikannya ke kasus umum Qk dan diimplementasikan di Jawa untuk membuktikan bahwa algoritme berfungsi.
Ide
Misalkan kita memiliki interval I arbitrer yang kita hanya tahu bahwa itu mengandung setidaknya satu dari angka yang hilang. Setelah satu lulus melalui array masukan, hanya melihat angka-angka dari saya , kita dapat memperoleh baik jumlah S dan kuantitas Q hilang nomor dari saya . Kami melakukan ini dengan hanya decrementing I panjang setiap kali kita jumpai nomor dari saya (untuk memperoleh Q ) dan dengan mengurangi pra-dihitung jumlah semua nomor di I dengan nomor ditemui setiap kali (untuk memperoleh S ).
Sekarang kita melihat S dan Q . Jika Q = 1 , itu berarti bahwa kemudian saya hanya berisi salah satu nomor yang hilang, dan jumlah ini jelas S . Kami menandai saya sebagai selesai (disebut "tidak ambigu" dalam program ini) dan meninggalkannya dari pertimbangan lebih lanjut. Di sisi lain, jika Q> 1 , kita dapat menghitung rata-rata A = S / Q nomor hilang terkandung dalam saya . Seperti semua nomor yang berbeda, setidaknya satu dari nomor tersebut secara ketat kurang dari A dan setidaknya satu ketat lebih besar dari A . Sekarang kita membagi saya di Amenjadi dua interval yang lebih kecil yang masing-masing berisi setidaknya satu nomor yang hilang. Perhatikan bahwa tidak masalah ke interval mana yang kami tetapkan A jika itu adalah bilangan bulat.
Kami membuat larik array berikutnya menghitung S dan Q untuk masing-masing interval secara terpisah (tetapi dalam lintasan yang sama) dan setelah itu menandai interval dengan Q = 1 dan membagi interval dengan Q> 1 . Kami melanjutkan proses ini sampai tidak ada interval "ambigu" baru, yaitu kami tidak memiliki apa-apa untuk dibagi karena setiap interval mengandung tepat satu nomor yang hilang (dan kami selalu tahu nomor ini karena kami tahu S ). Kita mulai dari satu-satunya interval "seluruh rentang" yang berisi semua angka yang mungkin (seperti [1..N] dalam pertanyaan).
Analisis kompleksitas waktu dan ruang
Jumlah lintasan p yang perlu kita buat sampai proses berhenti tidak pernah lebih besar dari jumlah angka yang hilang k . Ketidaksetaraan p <= k dapat dibuktikan dengan ketat. Di sisi lain, ada juga batas atas empiris p <log 2 N + 3 yang berguna untuk nilai besar k . Kita perlu melakukan pencarian biner untuk setiap nomor array input untuk menentukan interval miliknya. Ini menambah pengali log k ke kompleksitas waktu.
Secara total, kompleksitas waktu adalah O (N ᛫ min (k, log N) ᛫ log k) . Perhatikan bahwa untuk k besar , ini jauh lebih baik daripada metode sdcvvc / Dimitris Andreou, yaitu O (N ᛫ k) .
Untuk pekerjaannya, algoritma ini membutuhkan O (k) ruang tambahan untuk menyimpan pada interval paling k , yang secara signifikan lebih baik daripada O (N) dalam solusi "bitset".
Implementasi Java
Berikut adalah kelas Java yang mengimplementasikan algoritma di atas. Selalu mengembalikan diurutkan array nomor hilang. Selain itu, tidak memerlukan angka yang hilang, hitung k karena menghitungnya pada pass pertama. Seluruh rentang angka diberikan oleh parameter minNumber
dan maxNumber
(misalnya 1 dan 100 untuk contoh pertama dalam pertanyaan).
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;
}
}
Untuk keadilan, kelas ini menerima input dalam bentuk NumberBag
objek. NumberBag
tidak mengizinkan modifikasi larik dan akses acak dan juga menghitung berapa kali larik diminta untuk melintasi sekuensial. Ini juga lebih cocok untuk pengujian array besar daripada Iterable<Integer>
karena menghindari tinju int
nilai-nilai primitif dan memungkinkan membungkus sebagian besar int[]
untuk persiapan pengujian yang nyaman. Tidak sulit untuk mengganti, jika diinginkan, NumberBag
dengan int[]
atau Iterable<Integer>
mengetikkan find
tanda tangan, dengan mengubah dua for-loop di dalamnya menjadi masing-masing.
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();
}
};
}
}
Tes
Contoh sederhana yang menunjukkan penggunaan kelas-kelas ini diberikan di bawah ini.
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");
}
}
Pengujian array besar dapat dilakukan dengan cara ini:
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);
}
}
Cobalah di Ideone