第一章 算法概述
主定理
算法满足4条性质:
1.输入:有零个或多个由外部提供的量作为算法的输入
2.输出:至少一个量作为输出
3.确定性:每条指令是清晰的,无歧义的
4.有限性:每条指令的执行次数是有限的,执行时间也是有限的
算法与程序的区别:程序不要求满足性质4。
算法复杂度只依赖于要解的问题的规模、算法的输入和算法本身的函数。
Ω() <= T() <= O()
ω() < T() < o()
当Ω()=O()时,T() = θ()
(讲的比较不细致,大致明白就行)
第二章 递归与分治策略
分治法的基本思想:
将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各子问题的解合并到原问题的解。
最优子结构 & 无重复子问题(子问题独立)
平衡子问题的思想
分治法所能解决的问题一般具有的几个特征是:
(1)该问题的规模缩小到一定的程度就可以容易地解决;
(2)该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
(3)利用该问题分解出的子问题的解可以合并为该问题的解;
(4)原问题所分解出的各个子问题是相互独立的,即无重复子问题。
全排列
Perm(int list[], int k, int m) {
if(k == m) {
for(int i = 0;i <= m;i ++) cout << list[i];
cout << endl;
} else {
for(int i = k;i <= m;i ++) {
swap(list[i], list[k]);
Perm(list, k+1, m);
swap(list[i], list[k]);
}
}
}
Perm(0,n-1)是对list[0:n-1]全排列。
整数划分问题
int q(int n, int m) { // 将整数n划分成不超过m的若干个数
if(n < 1 || m < 1) return 0;
if(n == 1 || m == 1) return 1;
if(n < m) return q(n, n);
if(n == m) return q(n, m-1) + 1;
return q(n, m-1) + q(n-m, m);
}
棋盘覆盖
void ChessBoard(int tr, int tc, int dr, int dc, int size) { // 参数分别表示当前大方格左上角位置的row和col,特殊方格在当前大方格中特殊方格的位置row和col,棋盘大小
if(size == 1) return ;
int t = title ++;
int s = size / 2;
// 左上
if(dr < tr+s && dc < tc+s) ChessBoard(tr, tc, dr, dc, s);
else Board[tr+s-1][tc+s-1] = t, ChessBoard(tr, tc, tr+s-1, tc+s-1, s);
// 右上
if(dr < tr+s && dc >= tc+s) ChessBoard(tr, tc+s, dr, dc, s);
else Board[tr+s-1][tc+s] = t, ChessBoard(tr, tc+s, tr+s-1, tc+s, s);
// 左下
if(dr >= tr+s && dc < tc+s) ChessBoard(tr+s, tc, dr, dc, s);
else Board[tr+s][tc+s-1] = t, ChessBoard(tr+s, tc, tr+s, tr+s-1, s);
// 右下
if(dr >= tr+s && dc >= tc+s) ChessBoard(tr+s, tc+s, dr, dc, s);
else Board[tr+s][tc+s] = t, ChessBoard(tr+s, tc+s, tr+s, tc+s, s);
}
时间复杂度递归方程:
k为棋盘大小2的k次幂。
例题:
【注意:先标注的是2,后标注1】
【imp】合并排序
基本思想:
将待排序元素分成大小大致相同的两个集合,分别对两个子集进行排序,最终将排序号的子集合合并成要求的排好序的集合。
递归法:
未实现具体函数
void MergeSort(nt a[], int left, int right) {
if(left < right) { // 至少含2个元素
int mid = (left+right)/2;
MergeSort(a, left, mid); // 只是分割
MergeSort(a, mid+1, right);
Merge(a, b, left, i, right); // 合并到数组b
Copy(a, b, left, right); // 复制回数组a
}
}
非递归:
void MergeSort(int a[], int n) {
int *b = new int b[n];
int s = 1; // 控制每次合并的相邻区间的长度
while(s < n) {
MergePass(a, b, s, n); // 合并到b数组
s += s;
MergePass(b, a, s, n); // 合并到a数组
s += s;
}
}
void MergePass(int x[], int y[], int s, int n) {
int i = 0;
while(i <= n - 2*s) {
Merge(x, y, i, i+s-1, i+2*s-1); // 合并大小为s相邻两段子数组
i += 2*s;
}
if(s+i < n) // 剩余元素不足2s个
Merge(x, y, i, i+s-1, n-1); // 数组从0开始,因此为n-1
else // 剩余元素不足s个
for(int j = i;j < n;j ++)
y[j] = x[j];
}
void Merge(int c[], d[], int l, int m, int r) { // 合并c[l:m]和c[m+1:r]到d[l:r]
int i = l, j = m+1, k = l;
while(i<=m && k <= r) {
if(c[i] <= c[j]) d[k++] = c[i++];
else d[k++] = c[j++];
}
if(i > m) {
for(int t = j;t <= r;t ++)
c[k++] = c[t];
} else {
for(int t = i;t <= m;t ++)
c[k++] = c[t];
}
}
快速排序
void QuickSort(int a[], int p, int r) {
if(p < r) {
int q = Partition(a, p, r);
QuickSort(a, p, q-1);
QuickSort(a, q+1, r);
}
}
int Partition(int a[], int p, int r) {
int i = p, j = r+1;
int x = a[p];
while(true) {
while(a[++i] < x && i < r) ;
while(a[--j] > x) ;
if(i >= j) break;
swap(a[i], a[j]);
}
a[p] = a[j];
a[j] = x;
return j;
}
第三章 动态规划
基本思想:
与分治法类似,将待求解问题分解成若干子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,适合用动态规划法求解的问题经分解得到的子问题往往不是相互独立的。
最优子结构 & 重叠子问题
自上向下分析,自下向上解决
解体步骤:
1.找出最优解的性质,刻画其结构特征
2.递归定义最优值
3.自底向上求最优值
4.构造最优解
1~3为动态规划算法的基本步骤,在只需要最优值的情形下,步骤4可以省略
矩阵连乘
递归定义:
void MatrixChain(int *p, int n, int **m, int **s) {
for(int i = 1;i <= n;i ++) m[i][j] = 0;
for(int r = 2;r <= n;r ++)
for(int i = 1;i <= n-r+1;i ++) {
int j = i+r-1;
m[i][j] = m[i+1][j] + p[i-1] * p[i] * p[j];
s[i][j] = i;
for(int k = i+1;k < j;k ++) {
int t = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j];
if(t < m[i][j]) {
m[i][j] = t;
s[i][j] = k;
}
}
}
}
租用船艇问题
(多源最短路弗洛伊德)
(单源最短路迪杰斯特拉)
从第1站到第i站
凸多边形最优三角剖分
注意:
1、计算凸n+1边形的最优权值为t[1][n]
2、t[i][i]
为退化为两点的多边形,其权值为0
3、t[i][j]
表示多边形{vi-1,vi,…,vj}
的最优权值
4、代码与矩阵连乘几乎一致
n多边形三角剖分个数计算公式 f(3)=1; f(n)=∑ f(k)∗f(n+2−k) 其中n>3, n-1>=k>=3
0-1背包问题
递归定义:
时间复杂度:O(nc)
例题:
通过二维表看的时候,从右下角开始向上回溯,与正上面的数一样说明没被选,不一样说明选了,就可以向左前方回溯。
课后题1:P81 3-3
完全背包问题
for(int i = 1;i <= n;i ++)
for(int j = 0;j <= m;j ++)
for(int k = 0;k*w[i] <= j;k ++)
f[i][j] = max(f[i][j], f[i-1][j-k*w[i]] + k*v[i]);
// f[n][m]即为答案,n为物品个数,m为背包承重
课后题2 P81 3-4
给定n种物品和一背包。物品i的重量是wi,体积是bi,其价值为vi,背包的容量为C,容积为D。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或者不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。试设计一个解此问题的动态规划算法,并分析算法的计算复杂性。
for(int i = 1;i <= n;i ++)
for(int j = c;j >= w[i];j --)
for(int k = d;k >= b[i];k --)
f[j][k] = max(f[j][k], f[j-w[i]][k-b[i]] + v[i]);
完整代码:
/*
5 8 7
3 2 6
4 1 5
6 4 7
1 1 3
2 4 8
17
*/
// 未压位
#include<bits/stdc++.h>
using namespace std;
const int N = 50;
int f[N][N][N];
int n, c, d, ans;
int w[N], v[N], b[N];
int main()
{
cin >> n >> c >> d;
for(int i = 1;i <= n;i ++) cin >> w[i] >> b[i] >> v[i];
// 核心
for(int i = 1;i <= n;i ++)
for(int j = 0;j <= c;j ++)
for(int k = 0;k <= d;k ++) {
f[i][j][k] = f[i-1][j][k];
if(j >= w[i] && k >= b[i]) f[i][j][k] = max(f[i-1][j][k], f[i-1][j-w[i]][k-b[i]] + v[i]);
}
cout << f[n][c][d] << endl;
return 0;
}
// 压位
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int f[N][N];
int n, c, d;
int w[N], v[N], b[N];
int main()
{
cin >> n >> c >> d;
for(int i = 1;i <= n;i ++) cin >> w[i] >> b[i] >> v[i];
// 核心
for(int i = 1;i <= n;i ++)
for(int j = c;j >= w[i];j --)
for(int k = d;k >= b[i];k --)
f[j][k] = max(f[j][k], f[j-w[i]][k-b[i]] + v[i]);
cout << f[c][d] << endl;
return 0;
}
时间复杂度:O(ncd)
第四章 贪心
最优子结构 & 贪心选择性质
贪心选择性质定义:所求问题的整体最优解可以通过一系列局部最优解最优的选择,即贪心选择到达。
最优子结构定义:当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构。
活动安排问题
按结束时间最早的进行贪心。
void GreedySelector(int n, Type s[], Type f[], bool A[]) {
A[1] = true;
int j = 1;
for(int i = 2;i <= n;i ++) {
if(s[i] >= f[j]) {
A[i] = true;
j=i;
} else A[i] = false;
}
}
打水问题
贪心选择性质的证明:
最优子结构的证明:
多机调度
NPC问题。
有时可以通过贪心选择较好的近似解。
单源最短路问题
时间复杂度:O(n2)
(自己对Dijkstra的理解:无非就是每次选出与源点距离最近的点加入集合,再通过新加入的点去更新与其他未加入的点的距离,直至全部点加入集合)
理解思想后,写代码、背代码都容易。
void Dijkstra(int n, int v, int dist[], int prev[], int **edge) { // 点的个数、源点、源点与非集合点最短距离、上一个点、图初始距离
bool in[maxint];
for(int i = 1;i <= n;i ++) { // 初始化
dist[i] = edge[v][i];
in[i] = false;
if(edge[v][i] == maxint) prev[i] = 0;
else prev[i] = v;
}
in[v] = true;
dist[v] = 0;
for(int k = 1;k < n;k ++) {
int minedge = maxint;
int u = v;
for(int i = 1;i <= n;i ++) { // 选dist最小的加入集合
if(!in[i] && dist[i] < minedge) {
minedge = dist[i];
u = i;
}
}
in[u] = true;
for(int i = 1;i <= n;i ++) { // 更新不在集合中的点的dist
if(!in[i] && edge[u][i] < maxint) { // 通过新增入集合的点作为中转点去更新,要保证新增点与被更新点之间存在边
int t = dist[u] + edge[u][i];
if(t < dist[i]) {
dist[i] = t;
prev[i] = u;
}
}
}
}
}
最小生成树
Prim:
与Dijkstra大同小异啊!
void Prim(int n, int lowcost[], int closest[], int **edge) { // lowcost作用等同于Dijkstra中的dist,closest等同于prev用于构造最优解
bool s[maxint];
for(int i = 2;i <= n;i ++) {
lowcost[i] = edge[1][i];
s[i] = false;
closest[i] = 1; // 全部设为源点1
}
s[1] = true;
// closest[1] = 0; // 可有可无
for(int k = 1;i < n;k ++) {
for(int i = 2;i <= n;i ++) { // 因为默认从源点1开始扩展树,所以直接从第二个点开始的循环,下同
int minedge = maxint;
int u = 1;
if(!s[i] && lowcost[i] < minedge) {
minedge = lowcost[i];
u = i;
}
}
s[u] = true;
for(int i = 2;i <= n;i ++) {
if(!s[i] && edge[u][i] < lowcost[i]) {
lowcost[i] = edge[u][i];
closest[i] = u;
}
}
}
}
第五章 回溯法
剪枝函数分为约束函数和限界函数。
约束函数:在扩展结点处剪去不满足约束的子树(一般是不满足题目要求的)
限界函数;剪去得不到最优解的子树
解空间树分为子集树和排列树。
子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树;例如,0-1背包问题的解空间树,这类子集树通常有2n个叶子结点,总结点个数为2n+1-1;时间复杂度为Ω(2n)。
子集树一般都是一个元素为一层,代表选或不选。
排列树:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树;例如,旅行售货员问题;排列树通常有n!个叶子结点;时间复杂度为Ω(n!)。
回溯法搜索子集树的算法框架:
void Backtrack(int t) {
if(t > n) {
Output(x);
} else {
for(int i = 0;i <= 1;i ++) {
x[t] = i;
if(Constraint(t) && Bound(t)) {
Backtrack(t+1);
}
}
}
}
回溯法搜索排序树的算法框架:
void Backtrack(int t) {
if(t > n) {
Output(x);
} else {
for(int i = t;i <= n;i ++) {
Swap(x[t], x[i]);
if(Constraint(t) && Bound(t))
Backtrack(t+1);
Swap(x[t], x[i]);
}
}
}
装载问题
(理解即可,因为比较简单,理解了就能写出来)
void Backtrack(int t) {
if(t > n) {
if(bestw < cw) bestw = cw;
return ;
}
if(cw + w[t] <= c) { // 约束函数
cw += w[t];
Backtrack(t+1);
cw -= w[t];
}
Backtrack(t+1);
}
// 改进
void Backtrack(int t) {
if(t > n) {
bestw = cw;
return ;
}
r -= w[t];
if(cw + w[t] <= c) { // 约束函数
cw += w[t];
Backtrack(t+1);
cw -= w[t];
}
if(cw + r > bestw) // 限界函数
Backtrack(t+1);
r += w[t];
}
// 构造最优解
void Backtrack(int t) {
if(t > n) {
if(bestw < cw) {
for(int i = 1;i <= n;i ++) bestx[i] = x[i];
bestw = cw;
}
return ;
}
r -= w[t];
if(cw + w[t] <= c) { // 约束函数
x[t] = 1;
cw += w[t];
Backtrack(t+1);
cw -= w[t];
}
if(cw + r > bestw) { // 限界函数
x[t] = 0;
Backtrack(t+1);
}
r += w[t];
}
符号三角形
void Backtrack(int t) {
if(count > half || (t-1)*t/2 - count > half) return ;
if(t > n) ans ++;
else {
for(int i = 0;i < 2;i ++) { // 确定第一行第t个的符号,所在斜线的符号就自然确定了
p[1][t] = i;
count += i;
for(int j = 2;j <= t; j++) {
p[j][t-j+1] = p[j-1][t-j+1] ^ p[j-1][t-j+2];
count += p[j][t-j+1];
}
Backtrack(t+1);
for(int j = 2;j <= t;j ++)
count -= p[j][t-j+1];
count -= i;
}
}
}
符号三角形问题的变形:
void Backtrack(int t) {
// if(count > half || (t-1)*t/2 - count > half) return ; // 删除此句
if(t > n) ans ++;
else {
for(int i = 0;i < 2;i ++) {
p[1][t] = i;
count += i;
for(int j = 2;j <= t; j++) {
p[j][t-j+1] = p[j-1][t-j+1] ^ p[j-1][t-j+2];
count += p[j][t-j+1];
}
if(count <= half && (t+1)*t/2 - count <= half) // 添加此句
Backtrack(t+1);
for(int j = 2;j <= t;j ++)
count -= p[j][t-j+1];
count -= i;
}
}
}
N皇后
// x[i]表示第i行的皇后所在的列
bool Place(int k) {
for(int i = 1;i < k;i ++) { // 遍历前面的行
if(abs(k-i) == abs(x[k] - x[i]) || x[i] == x[k]) // 当两点纵坐标之差等于横坐标之差时,说明共斜线
return false;
return true;
}
}
void Backtrack(int t) {
if(t > n) sum ++;
else {
for(int i = 1;i <= n;i ++) {
x[t] = i;
if(Place(t))
Backtrack(t+1);
}
}
}
当解空间树为n叉树时,其显约束条件是任意一行只能安排一个皇后,其隐约束条件是任意一列和任意一斜线只能安排一个皇后(if条件中要规定的);
当解空间树为排列树时,其显约束条件是任意一行或任意一列只能安排一个皇后,其隐约束条件为任意一斜线只能安排一个皇后。
0-1背包问题
// cp为当前价值,cw为当前重量
// 首先按单位重量价值降序排序
void Backtrack(int i) {
if(i > n) {
bestp = cp;
return ;
}
if(cw + w[i] <= c) { // 选,进入左子树
cw += w[i];
cp += p[i];
Backtrack(i+1);
cw -= w[i];
cp -= p[i];
}
if(Bound(i+1) > bestp) // i+1,因为第i个物品不选,所以直接从i+1开始计算
Backtrack(i+1);
}
int Bound(int i) {
int cleft = c - cw;
int b = cp;
while(i <=n && w[i] <= cleft) {
cleft -= w[i];
b += p[i];
i++;
}
if(i <= n)
b += p[i]*cleft/w[i];
return b;
}
最大团问题
void Backtrack(int i) {
if(i > n) {
for(int j = 1;j <= n;j ++)
bestx[j] = x[j];
bestn = cn;
return ;
}
int OK = 1;
for(int j = 1;j < i;j ++) {
if(x[j] && a[i][j] == 0) {
OK = 0;
break;
}
}
if(OK) {
x[i] = 1;
cn ++;
Backtrack(i+1);
x[i] = 0;
cn --;
}
if(cn+n-i > bestn) {
x[i] = 0;
Backtrack(i+1);
}
}
旅行售货员问题
void Backtrack(int i) {
if(i == n) { // 不是 i > n 了!
if(a[x[n-1]][x[n]] != NoEdge && a[x[n]][1] != NoEdge &&
(cc + a[x[n-1]][x[n]] + a[x[n]][1] < bestc || bestc == NoEdge)) { // 直接用1代替x[1]是因为,主函数的Backtrack()从2开始调用的,保证了x[1]恒为第一个城市
for(int j = 1;j <= n;j ++)
bestx[j] = x[j];
bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];
}
} else {
for(int j = i;j <= n;j ++) { // 类似于全排列
if(a[x[i-1]][x[j]] != NoEdge && (cc + a[x[i-1]][x[j]] < bestc || bestc == NoEdge)) {
Swap(x[i], x[j]);
cc += a[x[i-1]][x[i]];
Backtrack(i+1);
cc -= a[x[i-1]][x[i]];
Swap(x[i], x[j]);
}
}
}
}
全排列的变形(去重的全排列,回溯)
void Backtrack(int t) {
if(t > n) {
for(int i = 1;i <= n;i ++) printf("%d", x[i]);
printf("\n");
} else {
for(int i = t;i <= n;i ++) {
int OK = 1;
for(int j = t;j < i;j ++) // i之前是否有与a[i]一样的,有说明换过了,无需再换
if(x[j] == x[i]) {
OK = 0;
break;
}
if(OK) {
swap(x[t], x[i]);
Backtrack(t+1);
swap(x[t], x[i]);
}
}
}
}
子集树对应的问题:最优装载问题、符号三角形问题、N皇后(上述代码是通过子集树求得的)、0-1背包问题(NPC)、最大团问题;
排列树对应的问题:批处理作业调度、旅行售货员问题;
第六章 分支限界
回溯法:深度优先搜索解空间
分支限界:广度优先或以最小消耗优先搜索解空间
分支限界法与回溯法的主要区别在于它们对当前扩展结点所采用的扩展方式不同。
在分支限界法中,每个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。此后,从活结点表中取下一结点作为扩展结点,重复过程直到找到所需的解或活结点表为空。
从活结点表中选取下一个扩展结点的不同方式产生了队列式(FIFO)分支限界法和优先队列式分支限界法。
最优装载
队列式:
int MaxLoading(int w[], int c, int n) {
Queue<int> Q;
Q.Add(-1);
int i = 1;
int bestw = 0,
Ew = 0,
r = 0;
for(int j = 2;j <= n;j ++) r += w[j]; // 剩余的箱子重量
while(true) {
int wt = Ew + w[i];
if(wt <= c) { // 类似于约束函数,选的情况,剪枝
if(wt > bestw) bestw = wt;
if(i < n) Q.Add(wt);
}
if(Ew + r > bestw && i < n) // 类似于限界函数,不选的情况,剪枝
Q.Add(Ew);
Q.Delete(Ew); // 扩展
if(Ew == -1) {
if(Q.Empty()) return bestw;
Q.Add(-1); // 同层结点尾标记
Q.Delete(Ew);
i ++;
r -= w[i];
}
}
}
构造最优解:
void MaxLoading(int w[], int c, int n, int bestx[]) {
Queue<QNode<int> *> Q;
Queue<int> *E = 0, *bestE;
Q.Add(0);
int i = 1;
int bestw = 0, Ew = 0, r = 0;
Q.Add(0);
for(int j = 2;j <= n;j ++) r += w[j];
while(true) {
int wt = Ew + w[i];
if(wt <= c) {
if(wt > bestw) wt = bestw;
EnQueue(Q, wt, i, n, bestw, E, bestE, bestx, true);
}
if(Ew + r > bestw) EnQueue(Q, Ew, i, n, bestw, E, bestE, bestx, false);
Q.Delete(E);
if(!E) {
if(Q.IsEmpty()) break;
Q.Add(0);
Q.Delete(E);
i ++;
r -= w[i];
}
Ew = E->weight;
}
for(int j = n-1;j > 0;j --) {
bestx[j] = bestE->LChild;
bestE = bestE->parent;
}
return bestw;
}
void EnQueue(Queue<QNode<int> *> &Q, int wt, int i, int n, int bestw, QNode<int> *E, QNode<int> * &bestE, int bestx[], bool ch) {
if(i == n) { // 叶子结点,不进入队列,所以直接从这里判断了
if(wt == bestw) {
bestE = E;
bestx[n] = ch;
}
return ;
}
// 非叶子结点
QNode<int> *b;
b = new QNode<int>;
b->weight = wt;
b->parent = E;
b->LChild = ch;
Q.Add(0);
}
class QNode { // 大致如此
QNode* parent;
int weight;
bool LChild;
}
优先队列式:
结论:
1.活结点x的优先级:根到结点x的路径相应的载重量+剩余集装箱重量之和
2.以结点x为根的子树中所有结点相应的路径的载重量不会超过x的优先级
3.叶子结点相应的载重量与其优先级相同
4.因此,一旦优先队列中有一个叶子结点成为扩展结点,则可以断定该叶子结点相应的解为最优解,算法终止(算法结束条件:将生成的叶子结点入优先队列,当叶子结点成为扩展结点时,算法结束)
5.与队列式分支限界法不同,优先队列分支限界法的叶子节点是要进入队列的
一些问题:
Q1:bestw 为何要在每次进入左分支时检查是否更新,而不是搜索至叶子结点时检查更新,解释其意义?
A1:尽早更新 bestw,目的使得右分支剪枝早些生效,不要等待到达叶子结点才更新 bestw
Q2:以什么为优先级?
A2:从根结点到结点x的路径所相应的载重量再加上剩余集装箱的重量之和(已选+未遍历到的剩余箱子重量之和)
布线问题
bool FindPath(Position start, Position finsih, int &PathLen, Position * &path) {
// 始末为一点
if(start.row == finish.row && start.col = finish.col) {
PathLen = 0;
return true;
}
// 设置围墙
for(int i = 0;i <= m+1; i++)
grid[0][i] = grid[n+1][i] = 1;
for(int i = 0;i <= n+1; i++)
grid[i][0] = grid[i][m+1] = 1;
// 初始化相对位移
Position offset[4];
offset[0].row = 0; offset[0].col = 1; // right
offset[1].row = 1; offset[1].col = 0; // down
offset[2].row = 0; offset[2].col = -1; // left
offset[3].row = -1; offset[3].col = 0; // up
int NumOfNbrs = 4;
Position here, nbr;
here.row = start.row;
here.col = start.col;
grid[start.row][start.col] = 2;
LinkedQueue<Position> Q;
do {
for(int i = 0;i < NumOfNbrs;i ++) {
nbr.row = here.row + offset[i].row;
nbr.col = here.col + offset[i].col;
if(grid[nbr.row][nbr.col] == 0) {
grid[nbr.row][nbr.col] = grid[here.row][here.col] + 1;
if(nbr.row == finish.row && nbr.col = finish.col) break;
Q.Add(nbr); // 添加扩展结点
}
}
if(nbr.row == finish.row && nbr.col = finish.col) break; // 完成布线
if(Q.IsEmpty()) return false; // 无解
Q.Delete(here);
} while(true);
// 构造最优解
PathLen = grid[finish.row][finsih.col] - 2; // 起点的grid为2
path = new Position[PathLen];
here = finfish;
for(int j = PathLen-1;j >= 0 ;j --) {
path[j] = here;
for(int i = 0;i < NumOfNbrs;i ++) {
nbr.row = here.row + offset[i].row;
nbr.col = here.col + offset[i].col;
if(grid[nbr.row][nbr.col] == j+2) break;
}
here = nbr;
}
return true;
}
限界函数:扩展到新位置的步数≥原位置记录的步数就剪枝
0-1背包
优先队列式分支限界算法中的优先级是通过Bound函数计算得到的,Bpund函数得到的是已选物品的价值与剩下空间用未遍历到的物品按单位重量价值从大到小选择填满背包所获得的价值最大值之和。
旅行售货员
禁忌:
1.对于刚扩展出的结点,其前已选的所有顶点是禁忌的(不能选);
2.对于未扩展出的结点,其前已选的(顶点1除外)的顶点是禁忌的。
第七章 随机化算法
随机化算法的一个基本特征是对所求问题的同一个实例用同一随机化算法求解两次可能得到完全不同的效果
数值随机化算法
1、常用于数值问题的求解;
2、近似解
3、近似解的精度随着计算时间的增加而不断提高
用法:
1、求圆周率
2、计算定积分
3、解非线性方程
蒙特卡洛算法
1、求问题的准确解
2、但是求出的未必是正确解
3、求得正确解的概率依赖于算法所用时间。算法所用时间越多,得到正确解的概率就越高
4、无法有效地判定所得解是否正确
补:
1、设p是实数且1/2<p<1。如果一个蒙特卡洛算法对于问题的任一实例得到正确解的概率不小于p,则称该蒙特卡洛算法是p正确的,且称p-1/2是该算法的优势。
2、如果对于同一个实例,蒙特卡洛算法不会给出两个不同的答案,则称该蒙特卡洛算法是一致的。
3、对于一致的p正确蒙特卡洛算法,要提高获得正确解的概率,只要执行该算法若干次并选择出现频次最高的解即可。
4、重复调用一致的、p正确、偏y0的蒙特卡洛算法k次,可得到一个1-(1-p)k正确的蒙特卡洛算法,且所得算法仍是一致的偏y0的蒙特卡洛算法。正确概率从p提高到1-(1-p)k。
用法:
1、主元素问题
2、素数测试
拉斯维加斯算法
1、得到的解一定是正确解,但有可能得不到解
2、找到正确解的概率随时间的增加而增加
3、对于所求解问题的任一实例,用同一拉斯维加斯算法反复对该实例求解足够多次,可使求解失效的概率任意小
4、拉斯维加斯+回溯 解决N后问题
用法:
1、N皇后
2、整数因子分解
舍伍德算法
1、总是能求得一个解,且所得解总是正确的
2、当一个确定性算法在最坏情况下的计算复杂性与其在平均情况下的计算复杂性有较大差别时,可在这个确定性算法中引入随机性将它改造成一个舍伍德算法
3、消除或减少问题的好坏实例间的这种差别
4、舍伍德算法的精髓是:设法消除这种最坏情况行为与特定实例之间的关联性
5、洗牌算法:
void Shuffle(Type a[], int n) {
static RandomNumber rnd;
for(int i = 0;i < n;i ++) {
int j = rnd.Random(n-i) + i; // 将a[i]与a[i]之后的数随机交换,实现打乱的效果
Swap(a[i], a[j]);
}
}
用法:
1、线性时间选择
2、搜索有序表
3、跳跃表
4、快速排序
P、NP、NPC、NP-Hard
一个问题A可以约化到问题B的含义是可以用求解B的方式求解A。
P:多项式时间内可解决
NP:多项式时间内可验证解
NPC:是一个NP问题,且所有NP问题都可以约化到它
⊂:真包含于,等于⫋
⊆:包含,可以相等
因此,P⊆NP,NPC⊆NP
盲猜的算法设计题
动态规划类
// 生奶牛、生兔子问题。可递归可递推
f[i] = f[i-1] + f[i-n];
// n为多少年生一只
// 环形石子合并(最大的得分,最小类似)
for(int i = 1;i <= 2*n;i ++) sum[i] = sum[i-1] + a[i];
for(int r = 2;r <= n;r ++)
for(int i = 1;i <= 2*n;i ++) {
for(int k = i;k < i+r-1;k ++)
f[i][j] = max(f[i][j], f[i][k] + f[k+1][i+r-1] + sum[i+r-1] - sum[i-1]);
for(int i = 1;i <= n;i ++)
ans = max(ans, f[i][i+n-1]);
// 数字三角形
// 自底向上
for(int i = n-1;i >= 1;i --)
for(int j = 1;j <= i;j ++)
dp[i][j] += max(dp[i+1][j], dp[i+1][j+1]);
// 自顶向下
for(int i = 2;i <= n;i ++)
for(int j = 1;j <= i;j ++)
dp[i][j] += max(dp[i-1][j], dp[i-1][j+1]);
for(int i = 1;i <= n;i ++) ans = max(ans, dp[n][i]);
// 将数n拆成至少两个数满足这些数之和为n,这些数之积最大,输出最大积
for(int i = 0;i <= n;i ++) dp[i] = 1;
for (int i = 3; i <= n;i ++)
for (int j = 1; j < i;j ++)
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]));
// 贪心解法:拆出3
int integerBreak(int n) {
if(n == 2) return 1;
if(n == 3) return 2;
int result=1;
while((n-3)>1){
result = result*3;
n = n-3;
}
result = result * n;
return result;
}
贪心类
// 硬币找零(加大一点难度,每个硬币个数有限制)(思路比较简单,看一下实现)
// 硬币为1,5,10,20,50,100,个数为a[0~5],面额为value[0~5]
for(int i = 5;i >= 0;i --) {
int t = min(a[i], money/value[i]);
money -= t*value[i];
ans += t;
}
/*
[问题描述]有N堆纸牌,编号分别为l,2,…,N。每堆上有若干张,但纸牌总数必为N的倍数。可以在任一堆上取若干张纸牌,然后移动。移牌规则为:在编号为1堆上取的纸牌,只能移到编号为2的堆上;在编号为N的堆上取的纸牌,只能移到编号为N+1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
*/
// 贪心思路:若第i堆少则从i+1堆补,若第i堆多,则送到第i+1堆中。
// 排队打水问题
// n个人,m个水龙头
sort();// 时间从小到大
for(int i = 0;i < m;i ++) b[i] = a[i];
for(int i = m;i < n;i ++) b[i] = b[i-m] + a[i];
for(int i = 0;i < n;i ++) ans += b[i];
补充
- 实现循环赛日程表利用的算法是分治策略
- Strassen 矩阵乘法是利用分治策略实现的算法
- 下列算法中通常以自底向下的方式求解最优解的是分治策略
- 备忘录方法是动态规划算法的变形
- 分支限界法解最大团问题时,活结点表的组织形式是最大堆
- 分支限界法解旅行售货员问题时,活结点表的组织形式最小堆
- 回溯法的效率不依赖于确定解空间的时间
- 0-1背包问题的回溯算法所需的计算时间为n*2n
- 采用最大效益优先搜索方式的算法是分支限界
- 解决 0-1 背包问题可以使用动态规划、回溯法和分支限界法,其中动态规划不需要排序,剩下两个需要排序
- N 皇后问题只使用约束条件进行裁剪
- 贪心选择性质是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别
- 分支限界法与回溯法的相同点是:都是一种在问题的解空间树 T 中搜索问题解的算法。
不同点:
(1)求解目标不同;
(2)搜索方式不同;
(3)对扩展结点的扩展方式不同;(主要区别)
(4)存储空间的要求不同。 - 多阶段决策问题就是要在可以选择的那些策略中间选取一个最优策略使在预定的标准下达到最好的效果。