1. Gambaran keseluruhan
Dalam tutorial ini, kita akan belajar bagaimana menyelesaikan beberapa masalah gabungan yang biasa. Mereka kemungkinan besar tidak berguna dalam pekerjaan seharian; namun, ia menarik dari perspektif algoritma. Kami mungkin menganggapnya berguna untuk tujuan ujian.
Perlu diingat bahawa terdapat banyak pendekatan yang berbeza untuk menyelesaikan masalah ini. Kami telah berusaha menjadikan penyelesaian yang dikemukakan mudah difahami.
2. Menjana Permutasi
Pertama, mari kita mulakan dengan permutasi. Permutasi adalah tindakan menyusun semula urutan sedemikian rupa sehingga mempunyai susunan yang berbeza.
Seperti yang kita ketahui dari matematik, untuk urutan unsur n , ada n! permutasi berbeza . n! dikenali sebagai operasi faktorial:
n! = 1 * 2 *… * n
Jadi, sebagai contoh, untuk urutan [1, 2, 3] terdapat enam permutasi:
[1, 2, 3] [1, 3, 2] [2, 1, 3] [2, 3, 1] [3, 1, 2] [3, 2, 1]
Factorial berkembang sangat cepat - untuk urutan 10 elemen, kami mempunyai 3,628,800 permutasi berbeza! Dalam kes ini, kita membincangkan peralihan urutan, di mana setiap elemen berbeza .
2.1. Algoritma
Adalah idea yang baik untuk memikirkan menghasilkan permutasi secara rekursif. Mari memperkenalkan idea negara. Ini akan terdiri daripada dua perkara: permutasi semasa dan indeks elemen yang sedang diproses.
Satu-satunya kerja yang perlu dilakukan dalam keadaan seperti itu adalah menukar elemen dengan setiap yang tinggal dan melakukan peralihan ke keadaan dengan urutan yang diubah dan indeks meningkat satu.
Mari kita gambarkan dengan contoh.
Kami ingin menghasilkan semua permutasi bagi urutan empat elemen - [1, 2, 3, 4] . Jadi, akan ada 24 permutasi. Ilustrasi di bawah menunjukkan langkah-langkah separa algoritma:

Setiap simpul pokok dapat difahami sebagai keadaan. Digit merah di bahagian atas menunjukkan indeks elemen yang sedang diproses. Digit hijau di nod menggambarkan pertukaran.
Jadi, kita mulakan di negeri ini [1, 2, 3, 4] dengan indeks sama dengan sifar. Kami menukar elemen pertama dengan setiap elemen - termasuk yang pertama, yang tidak menukar apa-apa - dan beralih ke keadaan seterusnya.
Sekarang, permutasi yang kita mahukan terletak di lajur terakhir di sebelah kanan.
2.2. Pelaksanaan Java
Algoritma yang ditulis dalam Java pendek:
private static void permutationsInternal(List sequence, List
results, int index) { if (index == sequence.size() - 1) { permutations.add(new ArrayList(sequence)); } for (int i = index; i < sequence.size(); i++) { swap(sequence, i, index); permutationsInternal(sequence, permutations, index + 1); swap(sequence, i, index); } }
Fungsi kami mengambil tiga parameter: urutan yang sedang diproses, hasil (permutasi), dan indeks elemen yang sedang diproses.
Perkara pertama yang perlu dilakukan ialah memeriksa sama ada kita telah mencapai elemen terakhir. Sekiranya demikian, kami menambahkan urutan ke senarai hasil.
Kemudian, dalam for-loop, kami melakukan pertukaran, melakukan panggilan berulang ke kaedah, dan kemudian menukar elemen kembali.
Bahagian terakhir adalah helah prestasi kecil - kita dapat beroperasi pada objek urutan yang sama sepanjang masa tanpa perlu membuat urutan baru untuk setiap panggilan berulang.
Mungkin juga idea yang baik untuk menyembunyikan panggilan rekursif pertama dengan kaedah fasad:
public static List
generatePermutations(List sequence) { List
permutations = new ArrayList(); permutationsInternal(sequence, permutations, 0); return permutations; }
Perlu diingat bahawa algoritma yang ditunjukkan hanya berfungsi untuk urutan elemen unik! Menerapkan algoritma yang sama untuk urutan dengan elemen berulang akan memberi kita pengulangan.
3. Menghasilkan Kekuatan Set
Masalah popular lain ialah menjana kuasa set. Mari kita mulakan dengan definisi:
poweret (atau power set) dari set S adalah himpunan semua subset dari S termasuk set kosong dan S itu sendiri
Oleh itu, sebagai contoh, diberikan satu set [a, b, c] , kumpulan kuasa mengandungi lapan subset:
[] [a] [b] [c] [a, b] [a, c] [b, c] [a, b, c]
Kita tahu dari matematik bahawa, untuk satu set yang mengandungi unsur n , rangkaian kuasa harus mengandungi 2 ^ n subset . Bilangan ini juga bertambah pesat, namun tidak secepat faktorial.
3.1. Algoritma
Kali ini, kita juga akan berfikir secara berulang. Sekarang, keadaan kita akan terdiri daripada dua perkara: indeks elemen yang sedang diproses dalam satu set dan penumpuk.
Kita perlu membuat keputusan dengan dua pilihan di setiap negeri: sama ada meletakkan elemen semasa dalam akumulator atau tidak. Apabila indeks kita mencapai akhir set, kita mempunyai satu subset yang mungkin. Dengan cara sedemikian, kita dapat menghasilkan setiap subset yang mungkin.
3.2. Pelaksanaan Java
Our algorithm written in Java is pretty readable:
private static void powersetInternal( List set, List
powerset, List accumulator, int index) { if (index == set.size()) { results.add(new ArrayList(accumulator)); } else { accumulator.add(set.get(index)); powerSetInternal(set, powerset, accumulator, index + 1); accumulator.remove(accumulator.size() - 1); powerSetInternal(set, powerset, accumulator, index + 1); } }
Our function takes four parameters: a set for which we want to generate subsets, the resulting powerset, the accumulator, and the index of the currently processed element.
For simplicity, we keep our sets in lists. We want to have fast access to elements specified by index, which we can achieve it with List, but not with Set.
Additionally, a single element is represented by a single letter (Character class in Java).
First, we check if the index exceeds the set size. If it does, then we put the accumulator into the result set, otherwise we:
- put the currently considered element into the accumulator
- make a recursive call with incremented index and extended accumulator
- remove the last element from the accumulator, which we added previously
- do a call again with unchanged accumulator and the incremented index
Again, we hide the implementation with a facade method:
public static List
generatePowerset(List sequence) { List
powerset = new ArrayList(); powerSetInternal(sequence, powerset, new ArrayList(), 0); return powerset; }
4. Generating Combinations
Now, it's time to tackle combinations. We define it as follows:
k-combination of a set S is a subset of k distinct elements from S, where an order of items doesn't matter
The number of k-combinations is described by the binomial coefficient:

So, for example, for the set [a, b, c] we have three 2-combinations:
[a, b] [a, c] [b, c]
Combinations have many combinatorial usages and explanations. As an example, let's say we have a football league consisting of 16 teams. How many different matches can we see?
The answer is , which evaluates to 120.
4.1. Algorithm
Conceptually, we'll do something similar to the previous algorithm for powersets. We'll have a recursive function, with state consisting of the index of the currently processed element and an accumulator.
Again, we've got the same decision for each state: Do we add the element to the accumulator?This time, though, we have an additional restriction – our accumulator can't have more than k elements.
It's worth noticing that the binomial coefficient doesn't necessarily need to be a huge number. For example:
is equal to 4,950, while
has 30 digits!
4.2. Java Implementation
For simplicity, we assume that elements in our set are integers.
Let's take a look at the Java implementation of the algorithm:
private static void combinationsInternal( List inputSet, int k, List
results, ArrayList accumulator, int index) { int needToAccumulate = k - accumulator.size(); int canAcculumate = inputSet.size() - index; if (accumulator.size() == k) { results.add(new ArrayList(accumulator)); } else if (needToAccumulate <= canAcculumate) { combinationsInternal(inputSet, k, results, accumulator, index + 1); accumulator.add(inputSet.get(index)); combinationsInternal(inputSet, k, results, accumulator, index + 1); accumulator.remove(accumulator.size() - 1); } }
This time, our function has five parameters: an input set, k parameter, a result list, an accumulator, and the index of the currently processed element.
We start by defining helper variables:
- needToAccumulate – indicates how many more elements we need to add to our accumulator to get a proper combination
- canAcculumate – indicates how many more elements we can add to our accumulator
Now, we check if our accumulator size is equal to k. If so, then we can put the copied array into the results list.
In another case, if we still have enough elements in the remaining part of the set, we make two separate recursive calls: with and without the currently processed element being put into the accumulator. This part is analogous to how we generated the powerset earlier.
Of course, this method could've been written to work a little bit faster. For example, we could declare needToAccumulate and canAcculumate variables later. However, we are focused on readability.
Again, a facade method hides the implementation:
public static List
combinations(List inputSet, int k) { List
results = new ArrayList(); combinationsInternal(inputSet, k, results, new ArrayList(), 0); return results; }
5. Summary
In this article, we've discussed different combinatorial problems. Additionally, we've shown simple algorithms to solve them with implementations in Java. In some cases, these algorithms can help with unusual testing needs.
Seperti biasa, kod sumber lengkap, dengan ujian, boleh didapati di GitHub.