0000 まえがき: 基本的に忘れがちなことを読んで試験の準備をするためのものです、主に後半の知識について、前半についてはアルゴリズム設計・解析段階試験の概要をご覧ください
第五章
バックトラッキング アルゴリズムは、問題の解決策を体系的に検索する方法です。問題の考えられるすべての解は、問題の解空間と呼ばれます。解空間が限られている場合、解空間はツリー構造にマッピングできます。バックトラッキング アルゴリズムの基本的な考え方は、1 つの道から進み、できる場合は進み、できない場合は戻り、別の道を試すというものです。
関連概念 1
- 拡張ノード
- スリップノード
- デッドノード
関連概念 2
- サブセット ツリー: サブセット ツリーを走査するには O(2n) の計算時間が必要です
- 順列ツリー: 順列ツリーを走査するには O(n!) の計算時間が必要です
第6章
分枝限定法は最適化問題を解くためのアルゴリズムで、通常は幅優先または最小コスト(最大利益)優先で問題の解空間木を探索します。基本的な考え方は、問題の実行可能な解決策を拡張し、各分岐から最適な解決策を見つけることです。
分岐限定方式では、分岐は幅優先戦略を使用して、拡張ノードのすべての分岐を順番に生成します。制限は、ノードの拡張プロセス中にノードの上限を計算し、探索中に一部の枝を切断することです。
バックトラッキングとの違い解決策の目標は異なります
- バックトラッキング方法は、制約を満たすすべての解を見つけることです。
- 分岐限定法とは、条件を満たす解、ある意味最適解を見つけることです。
別の方法で検索する
- バックトラッキング: 深さ優先
- 分岐限定法: 幅優先または最小コスト優先
関連概念
- スリップノード
- ライブノードテーブル PT
- 拡張ノード
- 息子ノード
- キューに入れられたブランチとバインドされたブランチ
- 優先キューの分岐と境界
第 7 章
ランダム化アルゴリズムは、大きく 4 つのカテゴリに分類されます。
- 数値ランダム化アルゴリズム
- モンテカルロアルゴリズム
- ラスベガスのアルゴリズム
- シャーウッドアルゴリズム
ランダム化アルゴリズムへの入力
- 元の問題への入力
- ランダムに選択された一連の乱数
数値確率的アルゴリズムは、数値問題を解決するためによく使用されます。これらのアルゴリズムは近似解を取得することが多く、計算時間が増加するにつれて近似解の精度も向上します。多くの場合、問題の正確な解を計算することは不可能または不必要ですが、数値課税標準アルゴリズムを使用すると、非常に満足のいく解を得ることができます。
数値問題は、さまざまな積分および微分の数学的計算でよく使用されます。
モンテカルロ アルゴリズムは、問題の正確な解決策を見つけるために使用されます。多くの問題では、近似解は無意味です。問題の解決策はモンテカルロ アルゴリズムで見つけることができますが、その解決策は正しくない可能性があります。正しい解が見つかる確率は、アルゴリズムにかかる時間によって異なります。アルゴリズムに時間がかかるほど、正しい解が得られる確率が高くなります。モンテカルロ アルゴリズムの主な欠点もここにあります。一般に、得られた解が決定可能で正しいかどうかを効果的に判断することは不可能です。(一般的以外の場合も判定可能です!)
ラスベガスのアルゴリズムは不正確な解を取得しません。ラスベガス アルゴリズムを使用して解が見つかると、それは正しい解である必要があります。しかし、ラスベガスのアルゴリズムでは解決策が見つからない場合があります。ラスベガスのアルゴリズムが正しい解を見つける確率は、必要な計算時間が増加するにつれて増加します。
シャーウッド アルゴリズムは 問題に対する正しい解決策を常に見つけることができます。アルゴリズムの最悪の場合の動作と特定のインスタンスの間の相関関係を排除しても、平均のパフォーマンスは向上しません。また、アルゴリズムの最悪の場合の動作を意図的に回避することもありません。
予防
- ランダム化アルゴリズムの結果が正しいことは保証できませんが、エラーの確率は制限できます。
- ランダム化アルゴリズムは、同じ入力インスタンスに対して異なる実行で異なる結果をもたらす可能性があるため、同じ入力インスタンスに対してランダム化アルゴリズムの実行時間が異なる場合があります。
アルゴリズムの分析
後戻り
バッチジョブのスケジューリング (推測)
n 個のジョブのセット {J1,J2,...,Jn} があるとします。各ジョブは、最初にマシン 1 で処理され、次にマシン 2 で処理される必要があります。ジョブ Ji はマシン j での処理時間 tij を必要とします。明確なジョブ スケジュールの場合、ジョブ i がマシン j で処理を完了する時刻を Fij とします。マシン 2 上のすべてのジョブの処理時間の合計は、ジョブ スケジュールの完了時間の合計と呼ばれます。バッチ ジョブ スケジューリングの問題では、指定された n 個のジョブに対して、完了時間の合計を最小化する最適なジョブ スケジューリング スキームを定式化する必要があります。
N以降の質問(省略)
互いに攻撃されない n 個のクイーンを n×n マス目のボード上に配置します。チェスのルールによれば、クイーンは、クイーンと同じ行または列、または同じ対角線上にある駒を攻撃できます。n クイーン問題は、n 個のクイーンを n×n グリッド上に配置することと同等であり、どの 2 つのクイーンも同じ行または列、または同じスラッシュ上には配置されません。この問題には、n = 1 または n ≥ 4 の場合にのみ解決策があります。
一連の考え
問題後の 4 つの解決策
記号三角形問題(省略)
一連の考え
n タプル x[1:n] を使用して、シンボル三角形の最初の行の n 個のシンボルを表します。x[i] が 1 に等しい場合、シンボルの最初の行の i 番目のシンボルが意味されます。三角形は「+」; x のとき [i] が 0 に等しい場合、シンボル三角形の最初の行の i 番目のシンボルが「-」であることを意味します; 1<=i<=n。x[i] は 2 値なので。したがって、バックトラッキング法を使用してシンボリック三角形問題を解く場合、完全な二分木を使用してその解空間を表すことができます。シンボル三角形の最初の行の最初の i 個のシンボル x[1:i] が決定された後、i*(i+1)/2 シンボルからなるシンボル三角形が決定されます。
(i*(i+1)/2 は、最初の項目が 1 で許容誤差が 1 である等差数列の合計公式から得られます)
解けない判定:n*(n+1)/2は奇数
0-1 ナップサック問題
演習: 0-1 ナップザック問題の例: n=4、c=16、p=[22,21,16,7]、w=[11,10,8,7]。バックトラッキング法に従ってこの問題を解決するには、次の質問に答えてみてください。
(1) この問題に対する制約関数は何ですか?
(2) 最適解を求めるための解空間木を描いてください。途中で破棄する必要があるノード(制約条件を満たさない解)を×、中間解を得たノードを一重丸○、最適解を二重丸◎で囲っている。
答え: (1) 制約関数は次のとおりです: ∑wixi≤C、つまり、バックパックはアイテムを保持できます。
(2) 解空間ツリーを下図に示します。
最大規模のグループ演習 (推測)
実現可能性制約関数: 現在の頂点から選択した頂点セットまでのすべての頂点がエッジによって接続されます。
(2) 境界関数: アルゴリズムが右側のサブツリー内でより大きなクリークを見つけることを可能にするのに十分なオプションの頂点があります。
枝と束縛
0-1 ナップサック問題
0-1 ナップサック問題の次の例を考えてみましょう: n=3、c=30、w=[16,15,15]、v=[45,25,25]
枝と束縛
巡回セールスマン問題(同じアイデアをPPTのみに載せています)(略)
キューに入れられたブランチとバインドされたブランチ
優先キューの分岐と境界
読み込みの問題も同じです
キューに入れられたブランチとバインドされたブランチ
プライオリティキューの分岐と結合方法(推測)
この例を解決するために優先キューの分岐限定法を使用すると、ライブノード テーブルの変更処理が行われます (優先度は現在の船にあるコンテナの重量と残りのコンテナの重量の合計です)。記述方法: [A,B,C]FGF(40)、ここで、[A,B,C] はアクティブなノード テーブル、A は現在の拡張ノード、FG は A によって生成され、G は条件を満たしません。制約があり、切断されます。40 はノード F の優先度を意味します
配線トラブルの例
ランダム化アルゴリズム
乱数 キャストポイント法による定積分の計算
モンテカルロランダム化アルゴリズム
プログラミング
後戻り
読み込みの問題
問題の説明
2 つの船にそれぞれ C1 と C2 の荷重を積んで積み込む n 個のコンテナのバッチがあり、コンテナ i の重量は wi であり、∑wi ≤ C1+C2 です。 積載問題では、次のような合理的な積載スキームがあるかどうかを判断する必要があります。これを積みます コンテナは 2 隻の船に積み込まれました。その場合は、読み込みスキームを見つけてください。特定の積載問題に解決策がある場合、次の戦略を採用することで最適な積載スキームが得られることを証明するのは簡単です: (1) まず、最初の船を可能な限り満杯にする; (2) 残りのコンテナを船に積み込む2番目の船の船。
問題分析
キーコード
// 搜索到叶子节点 if (i > n) { // 如果找到更优解,则更新最优解 if (cw > bestw) { bestw = cw; for (int j = 1; j <= n; j++) { best[j] = x[j]; } } return; } // 搜索左子树 r -= w[i]; if (cw + w[i] <= c) { x[i] = 1; cw += w[i]; backtrack(i + 1); cw -= w[i]; } r += w[i]; // 搜索右子树 if (cw + r > bestw) { x[i] = 0; backtrack(i + 1); }
詳細なコード
#include <stdio.h> #define MAX_N 20 int n; // 货物数量 int c; // 车的载重量 int w[MAX_N],x[MAX_N]; // 每个货物的重量 int best[MAX_N]; // 最优解 int cw; // 当前载重量 int bestw; // 最优载重量 int r; // 剩余物品重量和 // 搜索装载方案 void backtrack(int i) { // 搜索到叶子节点 if (i > n) { // 如果找到更优解,则更新最优解 if (cw > bestw) { bestw = cw; for (int j = 1; j <= n; j++) { best[j] = x[j]; } } return; } // 搜索左子树 r -= w[i]; if (cw + w[i] <= c) { x[i] = 1; cw += w[i]; backtrack(i + 1); cw -= w[i]; } r += w[i]; // 搜索右子树 if (cw + r > bestw) { x[i] = 0; backtrack(i + 1); } } int main() { //printf("请输入货物数量和车的载重量(用空格分隔):\n"); scanf("%d%d", &n, &c); //printf("请输入每个货物的重量:\n"); for (int i = 1; i <= n; i++) { scanf("%d", &w[i]); r += w[i]; } backtrack(1); printf("最优装载方案为:\n"); for (int i = 1; i <= n; i++) { if (best[i]) { printf("%d ", i); } } printf("\n最优载重量为:%d\n", bestw); return 0; }
0-1 バックパック ( PPT は醜くて水っぽいので、試験に推奨します · 次のコード)
PPT コードは、ブログ「Backtracking Algorithm Design Experiment」を参照してください。
キーコードはどこにありますか
int bound(int t) { int cleft = C - CurWeight;//剩余容量 int b = CurValue;//现阶段背包内物品的价值 while (t <= n && w[t] <= cleft)//以物品重量价值递减装入物品 { cleft = cleft - w[t]; b = b + v[t]; t++; } if (t <= n)//装满背包 b = b + v[t] * cleft / w[t];//计算t号物品的单位价值装满剩余空间 return b; } void backtrack(int t) { if (t > n)//到达叶子节点了 { if (CurValue > BestValue)//已经搜寻完一次了,把现有的最大值赋值; { BestValue = CurValue; for (int i = 1; i <= n; i++) BestX[i] = X[i]; } return; } if (CurWeight + w[t] <= C)//不到背包最大容量进入左子树 { X[t] = 1;//记录是否装入 CurWeight += w[t]; CurValue += v[t]; backtrack(t + 1);//回溯 CurWeight -= w[t]; CurValue -= v[t]; } if (bound(t + 1) > BestValue)//进入右子树 { X[t] = 0;//他自己没有后面物品合适 backtrack(t + 1);//判断 } }
問題の説明n 個のアイテムがあり、それぞれに体積と価値があります。与えられた容量のバックパックの場合、バックパックに積んだアイテムの合計価値を最大化するにはどうすればよいでしょうか?
注:通常のナップザック問題とは異なり、0-1 ナップザック問題ではアイテムが全体として表示され、全体をナップザックに入れるか、全体をナップザックに入れないかのみ選択できます。#include <stdio.h> #include <stdlib.h> #define N 100 int n, c, maxValue = 0; // 物品数量,背包容量,最大价值 int w[N], v[N]; // 物品重量,物品价值 int path[N]; int path0[N]; void backtrack(int i, int res, int value) { if (i == n) { if (value > maxValue) { maxValue = value; for (int i = 0; i < n; i++) path0[i] = path[i]; } return; } path[i] = 1; if (res >= w[i]) { backtrack(i + 1, res - w[i], value + v[i]); // 考虑第i个物品放入背包 } path[i] = 0; backtrack(i + 1, res, value); // 不考虑第i个物品放入背包 } int main() { scanf("%d%d", &n, &c); for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &v[i]); backtrack(0, c, 0); printf("%d\n", maxValue); for (int i = 0; i < n; i++) printf("%d ", path0[i]); return 0; }
巡回セールスマン問題(略)
問題分析
詳細なコード
#include <stdio.h> #include <stdbool.h> #define MAXN 100 // 最大城市数 int n; // 城市数 int graph[MAXN][MAXN]; // 图的邻接矩阵 int path[MAXN],bestPath[MAXN]; // 保存当前路径 bool visited[MAXN]; // 标记城市是否访问过 int minDist = 0x7fffffff; // 保存最短路径的长度 void backtracking(int cur, int dist) { if (cur == n) { // 所有城市都已经走过了 if (dist + graph[path[n - 1]][0] < minDist) { minDist = dist + graph[path[n - 1]][0]; // 更新最短路径 for(int i = 0;i < n;i++){ bestPath[i] = path[i]; } } return; } for (int i = 1; i < n; i++) { // 枚举下一个城市 if (!visited[i]) { // 如果这个城市还没有访问过 path[cur] = i; // 选择这个城市 visited[i] = true; // 标记这个城市已经访问过 backtracking(cur + 1, dist + graph[path[cur - 1]][i]); // 递归到下一层 visited[i] = false; // 回溯,撤销选择 } } } int main() { scanf("%d", &n); // 输入城市数 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { scanf("%d", &graph[i][j]); // 输入邻接矩阵 } } path[0] = 0; // 起点是城市0 visited[0] = true; // 标记起点已经访问过 backtracking(1, 0); // 从第2个城市开始递归 printf("%d\n", minDist); // 输出最短路径长度 for(int i = 0;i < n;i++){ printf("%d ",bestPath[i]+1); } return 0; }
0-1 バックパック
枝と束縛
読み込みの問題
// 定义MaxLoading子函数,用来求解装载问题 // 输入参数:w是一个整型数组,表示集装箱的重量;c是一个整型变量,表示船舶的载重量;n是一个整型变量,表示集装箱的数量;bestx是一个整型数组,用来存储最优解方案 // 返回值:bestw是一个整型变量,表示最优解值 int MaxLoading(int* w, int c, int n, int* bestx) { // 初始化变量 int i = 1; // 当前扩展节点所在层次 int j; // 循环计数器 int bestw = 0; // 最优解值 Heap h; // 优先队列 h.length = 0; // 优先队列的长度初始化为0 int* r = new int[n + 1]; // 剩余集装箱重量之和 r[n] = 0; for (j = n - 1; j > 0; j--) r[j] = r[j + 1] + w[j + 1]; Node* p = new Node; // 当前扩展节点 p->weight = 0; p->level = i; p->parent = NULL; Node* q; // 新生成节点 while (i != n + 1) { // 当还未到达叶子节点时循环 if (p->weight + w[i] <= c) { // 进入左子树,即选择第i个集装箱 q = new Node; // 创建新节点 q->LChild = 1; // 标记为左子树 q->level = p->level + 1; // 层次加一 q->parent = p; // 父节点指向当前扩展节点 q->weight = p->weight + w[i]; // 节点重量等于父节点重量加上第i个集装箱重量 q->uweight = q->weight + r[i]; // 节点上界等于节点重量加上剩余集装箱重量之和 if (q->level == n + 1 && q->weight > bestw) { // 找到更好解 bestw = q->weight; // 更新最优解值 for (j = n; j > 0; j--) { // 更新最优解方案 bestx[j] = q->LChild; q = q->parent; } } else { // 将新生成节点插入优先队列 HeapInsert(h, q); } } if (p->weight + r[i] > bestw) { // 进入右子树,即不选择第i个集装箱,并且满足剪枝条件 q = new Node; // 创建新节点 q->LChild = 0; // 标记为右子树 q->level = p->level + 1; // 层次加一 q->parent = p; // 父节点指向当前扩展节点 q->weight = p->weight; // 节点重量等于父节点重量 q->uweight = q->weight + r[i]; // 节点上界等于节点重量加上剩余集装箱重量之和 if (q->level == n + 1 && q->weight > bestw) { // 找到更好解 bestw = q->weight; // 更新最优解值 for (j = n; j > 0; j--) { // 更新最优解方案 bestx[j] = q->LChild; q = q->parent; } } else { // 将新生成节点插入优先队列 HeapInsert(h, q); } } delete p; // 删除当前扩展节点 if (!h.empty()) { // 取堆顶元素作为下一个扩展节点,并且堆不为空时继续循环 HeapDelete(h, p); i = p->level; } else { // 堆为空则结束循环 break; } } delete[] r; // 删除动态数组r return bestw; // 返回最优解值 }
最大クリーク問題
0-1 バックパック
ランダム化
ランダム化クイックソート: ピボット ポイントをランダムに選択するクイックソート アルゴリズム
コアコード
void quickSort(int r[], int low, int high) { srand(time(0)); int i, k; if (low<high) { i=randomNum(low, high); //在区间[low,high]中随机选取一个元素,下标为i r[low]←→r[i]; //交换r[low]和r[i]的值 k=partition(r, low, high); //进行一次划分,得到轴值的位置k quickSort(r, low, k-1);//在前半部分继续查找 quickSort(r, k+1, high);//在后半部分继续查找 } }
完全なコード
#include <stdio.h> #include <stdlib.h> #include <time.h> //舍伍德(Sherwood)型随机化算法 随机快速排序:随机选择枢点的快速排序算法 //在区间[low,high]中随机选取一个元素,下标为i int randomNum(int low, int high){ return low + rand() % (high - low + 1); } //交换两个元素的值 void swap(int *a, int *b){ int temp = *a; *a = *b; *b = temp; } //进行一次划分,得到轴值的位置k int partition(int r[], int low, int high){ int pivot = r[low]; //选取第一个元素作为轴值 while(low < high){ //循环直到low和high相遇 while(low < high && r[high] >= pivot) high--; //从右向左找到第一个小于轴值的元素 swap(&r[low], &r[high]); //交换r[low]和r[high]的值 while(low < high && r[low] <= pivot) low++; //从左向右找到第一个大于轴值的元素 swap(&r[low], &r[high]); //交换r[low]和r[high]的值 } return low; //返回轴值的位置 } //快速排序函数 void quickSort(int r[], int low, int high) { srand(time(0)); int i, k; if (low<high) { i=randomNum(low, high); //在区间[low,high]中随机选取一个元素,下标为i swap(&r[low], &r[i]); //交换r[low]和r[i]的值 k=partition(r, low, high); //进行一次划分,得到轴值的位置k quickSort(r, low, k-1);//在前半部分继续查找 quickSort(r, k+1, high);//在后半部分继续查找 } } //打印数组 void printArray(int arr[], int n){ int i; for(i=0; i<n; i++){ printf("%d ", arr[i]); } printf("\n"); } //主函数 int main(){ //定义一个数组,表示10个待排序的数 int arr[10] = {23, 45, 12, 67, 89, 34, 56, 78, 90, 11}; //打印原始数组 printf("原始数组:\n"); printArray(arr, 10); //调用快速排序函数 quickSort(arr, 0, 9); //打印排序后的数组 printf("排序后的数组:\n"); printArray(arr, 10); return 0; }
エイトクイーン問題(pptにはコードがないのでテストの確率は高くない気がします)
(1) 配列 x[8] を 0 に初期化し、試行回数を 0 に初期化します。
(2)for (i=1; i<=8; i++)
2.1 [1, 8] の乱数 j を生成します。
2.2 count=count+1、count 回目の試行を実行します。
2.3 クイーン i (行 i に固定) が競合せずに列 j に配置された場合、x[i]=j; count=0; ステップ (2) に進み (for ループは実行し続けます)、次のクイーンを配置します。
2.4 (count==8) の場合、クイーン i を配置できず、アルゴリズムは失敗します。
ステップ 2.1 に進み、クイーン i の位置を変更します。
(3) 8 クイーン問題の解として要素 x[1] ~ x[8] を出力します。
#include <iostream> #include <cstdlib> #include <ctime> using namespace std; bool isSafe(int x[], int row, int col) { // 检查当前位置是否与之前放置的皇后冲突 for (int i = 1; i < row; i++) { if (x[i] == col || abs(i - row) == abs(x[i] - col)) { return false; } } return true; } void solveEightQueens(int x[], int row) { if (row > 8) { // 所有皇后都放置完成,打印解 for (int i = 1; i <= 8; i++) { cout << x[i] << " "; } cout << endl; } else { for (int j = 1; j <= 8; j++) { if (isSafe(x, row, j)) { x[row] = j; solveEightQueens(x, row + 1); } } } } int main() { srand(time(0)); int x[9] = {0}; // 数组从下标 1 开始使用,初始化为0 solveEightQueens(x, 1); return 0; }
コードの出力の意味は次のとおりです。
- 各行はソリューション、つまり条件を満たすクイーン配置スキームを表します。
- 各行には 8 つの数字があり、それぞれ 1 行目から 8 行目までのクイーンの列番号を示します。
- たとえば、最初の行の出力は 4 2 7 3 6 8 5 1 です。これは、最初の行のクイーンが 4 列目にあり、2 行目のクイーンが 2 列目にあるということを意味します。
- 合計で 92 の可能な解決策、つまり 92 の異なるクイーン配置スキームがあります。
主な要素の問題
コアコード
bool isMajority(int arr[], int n, int x) { int count = 0; // 记录x出现的次数 for (int i = 0; i < n; i++) { if (arr[i] == x) count++; } return count > n / 2; // 如果x出现次数超过一半,返回true }
最適化
完全なコード
#include <iostream> #include <cstdlib> #include <ctime> using namespace std; // 在区间[low,high]中随机选取一个整数 int randomNum(int low, int high) { return low + rand() % (high - low + 1); } // 判断一个元素是否是主元素,即出现次数超过一半 bool isMajority(int arr[], int n, int x) { int count = 0; // 记录x出现的次数 for (int i = 0; i < n; i++) { if (arr[i] == x) count++; } return count > n / 2; // 如果x出现次数超过一半,返回true } // 蒙特卡罗函数,返回数组中的一个主元素,如果不存在,返回-1 int monteCarlo(int arr[], int n) { srand(time(0)); // 设置随机数种子 int k = 10; // 设置最大尝试次数 // 候选主元素初始化为数组的第一个元素 int candidate = arr[0]; int count = 1; // 记录候选主元素的计数 for (int i = 1; i < n; i++) { if (arr[i] == candidate) { count++; } else { count--; if (count == 0) { // 当前候选主元素计数为0,更新候选主元素为当前元素 candidate = arr[i]; count = 1; } } } // 最后确定的候选主元素需要再次验证 if (isMajority(arr, n, candidate)) { return candidate; // 如果是,返回该元素 } return -1; // 如果不存在主元素,返回-1 } // 打印数组 void printArray(int arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; } // 主函数 int main() { // 定义一个数组,表示n个待查找的数 int arr[10] = {3, 3, 4, 4, 2, 4, 2, 4, 4,4}; // 打印原始数组 cout << "原始数组:\n"; printArray(arr, 10); // 调用蒙特卡罗函数,返回数组中的一个主元素 int result = monteCarlo(arr, 10); // 打印结果 if (result == -1) { cout << "不存在主元素" << endl; } else { cout << "一个主元素是:" << result << endl; } return 0; }