【アルゴリズム設計・探索】バックトラッキング法の応用例(3)——順列・組み合わせ問題

9.完全順列問題(繰り返し数に関係なく)

【解説】全順列とは?たとえば、数値 1、2、3 の文字列が与えられた場合、それらの完全な配置は次のようになります。

  • 1,2,3
  • 1,3,2
  • 2,1,3
  • 2,3,1
  • 3,1,2
  • 3,2,1

この問題は、繰り返し数の場合を考慮していないことに注意してください。

【アルゴリズム解析】バックトラックの考え方を利用する。

ここに画像の説明を挿入

3 つの要素があると仮定すると、最初の状況は次のようになります。

i=0 i=1 i=2
0 1 2

注: 括弧は要素で、i=X は添え字です!

  • i=0を基準に後ろの他の要素と交換、まずはi=0(0 1 2)と交換。
    • i=1を基準に後ろの他の要素と交換、まずはi=1(0 1 2)で交換。
      • i=2 を基準に、後ろの他の要素と交換、最初に i=2 (0 1 2) と交換します。
      • このとき、i=2 には交換する要素が他にないので、シーケンス ( 0 1 2 ) を出力し、i=2 と i=2 を交換して戻します (0 1 2)。
    • i=1 に基づいて、最初に i=1 と i=1 を交換し (0 1 2)、その後、他の要素と交換を続け、i=2 (0 2 1) と交換します。
      • i=2 を基準に、後ろにある他の要素と交換し、最初に i=2 (0 2 1) と交換します。
      • このとき、i=2 には他に交換する要素がないので、シーケンス ( 0 2 1 ) を出力し、i=2 と i=2 を交換して戻します (0 2 1)。
    • この時点で、i=1 には他に交換する要素がないため、i=1 と i=2 を交換します (0 1 2)。
  • i=0 に基づいて、最初に i=0 と i=0 を交換し (0 1 2)、その後他の要素と交換を続け、i=1 (1 0 2) と交換します。
    • i=1 を基準に後ろの他の要素と交換、まず i=1 (1 0 2) と交換。
      • i=2 に基づいて、それぞれ後ろにある他の要素と交換し、最初に i=2 (1 0 2) と交換します。
      • このとき、i=2 には他に交換する要素がないので、シーケンス ( 1 0 2 ) を出力し、i=2 と i=2 を交換して戻します (1 0 2)。
    • i=1 に基づいて、最初に i=1 と i=1 を交換し (1 0 2)、その後他の要素と交換を続け、i=2 と交換します (1 2 0)。
      • i=2 を基準に、それぞれ後ろの他の要素と交換し、最初に i=2 (1 2 0) と交換します。
      • このとき、i=2 には他に交換する要素がないので、シーケンス ( 1 2 0 ) を出力し、i=2 と i=2 を交換して戻します (1 2 0)。
    • この時点で、i=1 には他に交換する要素がないため、i=1 と i=2 を交換します (1 0 2)。
  • i=0 に基づいて、最初に i=0 と i=1 を交換し (0 1 2)、その後他の要素と交換を続け、i=2 (2 1 0) と交換します。
    • i=1 に基づいて、それぞれ後ろにある他の要素と交換し、最初に i=1 (2 1 0) と交換します。
      • i=2 に基づいて、それぞれ後ろの他の要素と交換し、最初に i=2 (2 1 0) と交換します。
      • このとき、i=2 には他に交換する要素がないため、シーケンス ( 2 1 0 ) を出力し、i=2 と i=2 を交換して戻します (2 1 0)。
    • i=1 に基づいて、最初に i=1 と i=1 を交換し (2 1 0)、その後他の要素と交換を続け、i=2 (2 0 1) と交換します。
      • i=2 を基準に、後ろの他の要素と交換し、最初に i=2 (2 0 1) と交換します。
      • このとき、i=2 には他に交換する要素がないので、シーケンス ( 2 0 1 ) を出力し、i=2 と i=2 を交換して戻します (2 0 1)。
    • この時点で、i=1 には他に交換する要素がないため、i=1 と i=2 を交換します (2 1 0)。
  • この時点で、i=0 には他に交換する要素がなく、実行は終了します。

1,2,3,4 があると仮定すると、次のようになります。

  • まず、1 はそのままにして、2、3、4 をすべて並べます。
  • 2 はそのままにして、3 と 4 をすべて並べます。
  • 3 を変更せずに、4 つの完全順列に対して、4 の完全順列は 1 つだけです。1,2,3,4 を取得します。
  • 2 は変更せず、3 と 4 を交換して 1、2、4、3 を取得します。
  • 1 と 2 の配置が完了したら、3 の位置を 2 に変更し、上記 2 つの手順の操作を続けます。

【アルゴリズム説明】

void trace (int init){
    
    
    如果下标 init 超出了数组最大下标
        输出方案
    否则
        循环下标 i: init~数组最大下标
            交换 A[init], A[i]
            trace(init+1)
            交换 A[init], A[i]  // 回溯,为下一次交换做准备
}

【答え】

#include <cstdio>
using namespace std;

void swap (int &x, int &y){
    
    
	int tmp = x;
	x = y;
	y = tmp;
}

void trace (int A[], int size, int init){
    
    
	if (init == size){
    
    
		for (int j = 0; j < size; j++)
			printf("%d ", A[j]);
		printf("\n");
	}
	else
		for (int i = init; i < size; i++){
    
    
			swap(A[init], A[i]);
			trace(A, size, init+1);
			swap(A[init], A[i]);
		}
}

int main(){
    
    
	int n;
	int A[10] = {
    
    0};
	while (scanf("%d", &n) != EOF){
    
    
		for (int i = 0; i < n; i++)
			scanf("%d", &A[i]);
		printf("全排列:\n");
		trace(A, n, 0);
	}
	return 0;
}

10. 全順列問題(繰り返し数を考える)

【解説】繰り返し数を考慮した全順列とは?たとえば、数値 1、2、2 の文字列が与えられた場合、それらの完全な配置は次のようになります。

  • 1,2,2
  • 2,1,2
  • 2,2,1

ご覧のとおり、重複を排除しました。

[分析] 要素が 4 つあると仮定すると、初期状況は次のようになります。

i=0 i=1 i=2 i=3
0 1 2 2

i=0 は i=1、i=2 と入れ替えても問題ありませんが、i=3 と入れ替えると重複します。

i=1 は問題なく i=2 と交換されますが、i=3 と交換すると重複があります。

別の例として、5 つの要素があり、最初の状況が次のようになっているとします。

i=0 i=1 i=2 i=3 i=4
0 1 2 3 2

i=0 は i=1、i=2、i=3 とスワップしても問題ありませんが、i=4 とスワップすると重複します。

i=1 は i=2、i=3 と入れ替えても問題ありませんが、i=4 と入れ替えると重複が発生します。

i=2 と i=4 を入れ替えると重複が発生します。

上記の 2 つの例を確認してください。重複があるのはなぜですか? i=X との交換を繰り返すと、i=X の前に交換された同じ要素が存在するはずだからです! したがって、交換する要素が以前に繰り返されたかどうかを判断するだけで十分です!

2 番目の例を使用して、i=0 を i=1、i=2、i=3、および i=4 を以前に交換された i=2 と交換しても問題ないことを示します。i=0~4 の区間に繰り返し要素があることを示しています。

i=1 を i=2、i=3、i=4 と交換しても問題ありません。区間 i=1~4 に繰り返し要素があることを示しています。

【答え】

bool isSwap (int A[], int init, int end){
    
    
	for (int i = init; i < end; i++) // 判断基准元素init到要交换元素end的区间内,有没有跟要交换元素end重复的元素 
		if (A[i] == A[end])
			return false;
	return true;
}

void trace (int A[], int size, int init){
    
    
	if (init == size){
    
    
		for (int j = 0; j < size; j++)
			printf("%d ", A[j]);
		printf("\n");
	}
	else
		for (int i = init; i < size; i++){
    
    
			if (isSwap(A, init, i)){
    
      // 判断基准元素init到要交换元素i的区间内,有没有跟要交换元素i重复的元素 
				swap(A[init], A[i]);
				trace(A, size, init+1);
				swap(A[init], A[i]);
			}
		}
}

11. 組み合わせ問題(同じ数字を違う順番で取り出すと別の組み合わせとみなす)

【解説】コンビネーションとは?たとえば、4 つの数字 (1,2,3,4) から 3 つの数字を抽出するには、次の方法があります。

  • 1,2,3
  • 1,2,4
  • 1,3,2
  • 1,3,4
  • 1,4,2
  • 1,4,3
  • 2,1,3
  • 2,1,4
  • 2,3,1
  • 2,3,4
  • 2,4,1
  • 2,4,3
  • 3,1,2
  • 3,1,4
  • 3,2,1
  • 3,2,4
  • 3,4,1
  • 3,4,2
  • 4,1,2
  • 4,1,3
  • 4,2,1
  • 4,2,3
  • 4,3,1
  • 4,3,2

知らせ:

  • いくつかの組み合わせは、同じ数を異なる順序で取り出し、それらはすべて異なる組み合わせと見なされます。
  • 重複した番号は考慮されません。

【入出力サンプル】

4 2 
1 2 3 4
取出2个的组合:
1 2
1 3
1 4
2 1
2 3
2 4
3 1
3 2
3 4
4 1
4 2
4 3

【分析】バックトラックの考え方を利用して、数字を取り出すたびにその数字が使用済みであることをマークし、次の数字を取り出そうとします。その番号がすでに使用されている場合は、引き続き次の番号を試してください。

  • result配列を使用して、取得した数値を記録します。
  • used添え字に対応する要素が取り出されたかどうかを配列で記録します

【バックトラッキングアルゴリズムの説明】

// 取第 cnt 个数字
void trace (int cnt){
    
    
    如果 cnt == 取出的个数
        输出方案
    否则
        尝试数组 A 的每一个数字
            如果该数字没被取出过
                取出数字
                标记该数字已被取出
                trace(cnt+1); // 取第 cnt+1 个数字
                放回数字
                标记该数字没有被取出
}

【答え】

#include <cstdio>
using namespace std;

#define MAX 10

// A: 原数组, used: 记录下标对应的元素是否已取出, size: 数组A的长度
// num: 取出多少个数字, cnt: 当前取出第几个数字 
void trace_dict (int A[], bool used[], int size, int num, int cnt){
    
    
	static int result[MAX] = {
    
    0};
	// 若已经取出 num 个数字,则说明已经完成要求,输出方案 
	if (cnt == num+1){
    
      
		for (int i = 1; i <= num; i++)
			printf("%d ", result[i]);
		printf("\n");
		return;
	}
	// 尝试从数组 A 取出每一个数字 
	for (int i = 0; i < size; i++){
    
    
		if (!used[i]){
    
      // 如果没有取出该数字 
			result[cnt] = A[i];  // 取出数字 
			used[i] = true;      // 下标对应的元素已取出
			trace_dict(A, used, size, num, cnt+1);  // 开始取出第 cnt+1 个数字 
			result[cnt] = 0;     // 放回数字 
			used[i] = false;     // 下标对应的元素未取出
		}
	}
}

int main(){
    
    
	int A[MAX] = {
    
    0};
	bool used[MAX] = {
    
    false};
	int size, num;
	while (scanf("%d%d", &size, &num) != EOF){
    
    
		for (int i = 0; i < size; i++)
			scanf("%d", &A[i]);
		printf("取出%d个的组合:\n", num);
		trace_dict(A, used, size, num, 1);  // 初始为取出第 1 个数字 
		printf("\n");
	}
	return 0;
}

12.組み合わせ問題(同じ数字で順番が違うものを同じ組み合わせとして取り出す)

【解説】コンビネーションとは?たとえば、4 つの数字 (1,2,3,4) から 3 つの数字を抽出するには、次の方法があります。

  • 1,2,3
  • 1,2,4
  • 1,3,4
  • 2,3,4

知らせ:

  • いくつかの組み合わせは、同じ番号を異なる順序で取り出しますが、それらはすべて同じ組み合わせと見なされるため、非常に少ないです。
  • 重複した番号は考慮されません。

[解析] 添字 i 番目の数を取り出した後、次の数を取り出すとき、区間 [i+1, size-1] (サイズは配列のサイズ) 内の数のみを取り出して、数字を取るたびにこうすることで、組み合わせの重複を避けることができます。

【答え】

#include <cstdio>
using namespace std;

#define MAX 10

// A: 原数组, used: 记录下标对应的元素是否已取出, size: 数组A的长度
// num: 取出多少个数字, cnt: 当前取出第几个数字, pos: 取出的位置 
void trace(int A[], bool used[], int size, int num, int cnt, int pos){
    
    
	static int result[MAX] = {
    
    0};
	// 若已经取出 num 个数字,则说明已经完成要求,输出方案 
	if (cnt == num+1){
    
      
		for (int i = 1; i <= num; i++)
			printf("%d ", result[i]);
		printf("\n");
		return;
	}
	// 尝试从数组 A 的 pos 位置开始取出每一个数字 
	for (int i = pos; i < size; i++){
    
    
		if (!used[i]){
    
      // 如果没有取出该数字 
			result[cnt] = A[i];  // 取出数字 
			used[i] = true;      // 下标对应的元素已取出
			trace(A, used, size, num, cnt+1, i+1);  // 开始取出第 cnt+1 个数字,从下标为 i+1 开始取数
			result[cnt] = 0;     // 放回数字 
			used[i] = false;     // 下标对应的元素未取出
		}
	}
}

int main(){
    
    
	int A[MAX] = {
    
    0};
	bool used[MAX] = {
    
    false};
	int size, num;
	while (scanf("%d%d", &size, &num) != EOF){
    
    
		for (int i = 0; i < size; i++)
			scanf("%d", &A[i]);
		printf("取出%d个的组合:\n", num);
		trace(A, used, size, num, 1, 0);  // 初始为取出第 1 个数字,从下标为 0 开始取数 
		printf("\n");
	}
	return 0;
}

13.部分集合問題(集合代数における部分集合の定義)

【解説】サブセットとは?セット {1,2,3,4} があり、そのサブセットが次のとおりであるとします。

  • {1}
  • {1,2}
  • {1,3}
  • {1,4}
  • {1,2,3}
  • {1,2,4}
  • {1,3,4}
  • {1,2,3,4}
  • {2}
  • {2,3}
  • {2,4}
  • {2,3,4}
  • {3}
  • {3,4}
  • {4}

知らせ:

  • 集合の定義によれば、抽出されたいくつかの数は同じだが順序が異なり、それらはすべて同じ部分集合と見なされます。
  • セットの定義によれば、サブセットは適切なサブセットと等しくないため、1、2、3、4 もサブセットと見なされます。
  • 重複した番号は考慮されません。
  • 空集合は出力されません。

【解析】 最も簡単なアイデアとしては、組み合わせ問題のプログラムをそのまま使って、1数を取り出す、2数を取り出す、3数を取り出す、などのスキームを出力するというものがあります。

【答え】

#include <cstdio>
using namespace std;

#define MAX 10

// A: 原数组, used: 记录下标对应的元素是否已取出, size: 数组A的长度
// num: 取出多少个数字, cnt: 当前取出第几个数字, pos: 取出的位置 
void trace(int A[], bool used[], int size, int num, int cnt, int pos){
    
    
	static int result[MAX] = {
    
    0};
	// 若已经取出 num 个数字,则说明已经完成要求,输出方案 
	if (cnt == num+1){
    
      
		printf("{");
		for (int i = 1; i <= num; i++){
    
    
			if (i != num)
				printf("%d,", result[i]);
			else
				printf("%d}", result[i]);
		}
		printf("\n");
		return;
	}
	// 尝试从数组 A 的 pos 位置开始取出每一个数字 
	for (int i = pos; i < size; i++){
    
    
		if (!used[i]){
    
      // 如果没有取出该数字 
			result[cnt] = A[i];  // 取出数字 
			used[i] = true;      // 下标对应的元素已取出
			trace(A, used, size, num, cnt+1, i+1);  // 开始取出第 cnt+1 个数字,从下标为 i+1 开始取数
			result[cnt] = 0;     // 放回数字 
			used[i] = false;     // 下标对应的元素未取出
		}
	}
}

int main(){
    
    
	int A[MAX] = {
    
    0};
	bool used[MAX] = {
    
    false};
	int size;
	while (scanf("%d", &size) != EOF){
    
    
		for (int i = 0; i < size; i++)
			scanf("%d", &A[i]);
		for (int i = 1; i <= size; i++){
    
    
			printf("取出%d个的子集:\n", i);
			trace(A, used, size, i, 1, 0);  // 目标是取出 i 个数字,初始为取出第 1 个数字,从下标为 0 开始取数 
		}
		printf("\n");
	}
	return 0;
}

【入出力】

4
1 2 3 4
取出1个的子集:
{1}
{2}
{3}
{4}
取出2个的子集:
{1,2}
{1,3}
{1,4}
{2,3}
{2,4}
{3,4}
取出3个的子集:
{1,2,3}
{1,2,4}
{1,3,4}
{2,3,4}
取出4个的子集:
{1,2,3,4}

おすすめ

転載: blog.csdn.net/baidu_39514357/article/details/129615331