文章目录
目录
前言
王晓东<计算机算法设计与分析>第五章回溯代码复现
包含内容:
回溯法算法框架;装载问题;批处理作业调度;符号三角形问题;n后问题;0-1背包问题;最大团问题;图的m着色问题;旅行售货员问题;圆排列问题
如果可以在OJ中找到题目,则OJ和题目编号在注释中给出(如HDU2510表示杭电OJ2510号题目)。
如果不能在OJ中找到题目,则自拟了输入输出(如批处理作业调度)或给出核心求解代码(如圆排列问题)
均使用回溯求解的方法求解。
内容可供大家参考,如有问题希望不吝赐教!
一、回溯法的算法框架
// 递归回溯框架
void BackTrack(int t)
{
if (t > n)
{
// 搜索完了输出答案
Output(x);
}
else
{
for (int i = f(n, t); i <= g(n, t); i++)
{
// f(n, t) ~ g(n, t)当前扩展结点为搜索过的子树
// h(i)表示当前扩展结点的第i个可选值
x[t] = h(i);
if (Constraint(t) && Bound(t))
{
// 满足约束函数和边界函数
BackTrack(t + 1);
}
}
}
}
// 迭代回溯框架
void InterativeBacktract()
{
int t = 1;
while (t > 0)
{
if (f(n, t) <= g(n, t))
{
// 当前扩展结点是否还有未搜索过的子树
for (int i = f(n, t); i <= g(n, t); i++)
{
// 循环遍历当前结点还未搜索过的子树
x[t] = i;
if (Constraint(t) && Bound(t))
{
if (Solution(t)) Output(x);
else t++;
}
}
}
else
{
// 当前扩展结点没有未搜索过的子树,所以回溯
t--;
}
}
}
// 子集树
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]);
}
}
}
二、各题目
1.装载问题
/* *************** Loading Problem - BackTrack **************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
*************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N = 100;
void scanData();
bool canSolve();
void backTrack(int dep);
void printAns();
// 集装箱数、货船1、2载重, 各集装箱重
int n, c1, c2, w[N];
// 当前最优载重, 最重集装箱重, 集装箱总重
int bestw, maxw, totalw;
int cw, remain;
// 当前最优装载策略, 当前正在搜索的装载策略
bool bestStrategy[N], nowStrategy[N];
int main()
{
scanData(); // 输入数据
if (!canSolve())
{
// 不可解直接输出
puts("没有可行解!");
}
else
{
backTrack(1);
printAns();
}
system("pause");
return 0;
}
void scanData()
{
cout << "货船载重: ";
cin >> c1 >> c2;
cout << "集装箱数: ";
cin >> n;
cout << "各集装箱重: ";
for (int i = 1; i <= n; i++)
{
cin >> w[i];
maxw = max(maxw, w[i]);
totalw += w[i];
}
// remain表示还未讨论的货物总重量,初值为totalw
remain = totalw;
}
bool canSolve()
{
// 当集装箱总重小于货船总容量 且
// 最大货船容量大于最大集装箱容量时才有解
return totalw <= c1 + c2 && max(c1, c2) >= maxw;
}
void backTrack(int dep)
{
if (dep > n)
{
// 到达边界,更新答案
bestw = cw;
memcpy(bestStrategy, nowStrategy, sizeof(bool) * (n + 1));
return;
}
remain -= w[dep];
if (cw + w[dep] <= c1)
{
// 可行性剪枝
nowStrategy[dep] = true;
cw += w[dep];
backTrack(dep + 1);
cw -= w[dep];
}
if (cw + remain > bestw)
{
// 最优性剪枝
nowStrategy[dep] = false;
backTrack(dep + 1);
}
remain += w[dep];
}
void printAns()
{
if (totalw - bestw > c2)
{
puts("没有可行解!");
}
else
{
printf("货船1载重: ");
for (int i = 1; i <= n; i++)
{
if (bestStrategy[i])
{
cout << w[i] << " ";
}
}
puts("");
printf("货船2载重: ");
for (int i = 1; i <= n; i++)
{
if (!bestStrategy[i])
{
cout << w[i] << " ";
}
}
puts("");
}
}
2.批处理作业调度
/* *************** 批处理作业调度 - BackTrack **************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
**********************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 100;
const int INF = 0x3f3f3f3f;
// 机器1完成处理的时间, 机器2完成处理的时间, 总耗费时间
int f1, f2[N], f;
// 当前最优解,当前最优调度
int bestf, bestx[N];
// 作业数, 各作业所需时间表, 当前作业调度
int n, M[N][2], x[N];
void Backtrack(int dep);
int main()
{
// 输入数据
cout << "作业数: ";
cin >> n;
cout << "各作业所需机器1、2处理时间(一行两个用空格间隔的数字)\n";
for (int i = 1; i <= n; i++)
{
cin >> M[i][0] >> M[i][1];
}
// 初始化数据
bestf = INF;
for (int i = 1; i <= n; i++)
x[i] = i;
Backtrack(1);
// 打印答案
printf("最少耗费时间为: %d\n", bestf);
puts("最优调度为:");
for (int i = 1; i <= n; i++)
{
printf("%d ", bestx[i]);
}
puts("");
system("pause");
return 0;
}
void Backtrack(int dep)
{
if (dep > n)
{
// 到达边界,更新答案
bestf = f;
memcpy(bestx, x, sizeof(int) * (n + 1));
return;
}
for (int i = dep; i <= n; i++)
{
// 第dep个作业是x[i]的情况
// 上一个作业使用完机器1, 当前作业即可使用机器2
f1 += M[x[i]][0];
// 上一个作业用完机器2(f2[dep - 1])
// 且当前作业且当前作业接受完机器1的处理(f1), 才可使用机器2
f2[dep] = (f2[dep - 1] > f1 ? f2[dep - 1] : f1) + M[x[i]][1];
// 当前作业结束的标志是: 当前作业接受完机器2的处理(f2[dep])
f += f2[dep];
if (f < bestf)
{
// 最优性剪枝, 结果不可能更优
swap(x[dep], x[i]);
Backtrack(dep + 1);
swap(x[dep], x[i]);
}
// 还原现场
f1 -= M[x[i]][0];
f -= f2[dep];
}
}
3.符号三角形问题
/* *********** Symbolic Triangle(HDU2510) - BackTrack **********
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
***************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
int main()
{
int n;
int ans[N] = {0, 0, 0, 4, 6, 0, 0, 12, 40, 0, 0, 171, 410, 0, 0,
1896, 5160, 0, 0, 32757, 59984, 0, 0, 431095, 822229};
while (scanf("%d", &n) && n)
{
printf("%d %d\n", n, ans[n]);
}
system("pause");
return 0;
}
// *计算打表数据的求解代码
/*
// 记录符号三角形
bool p[N][N];
// 层数, 总符号数的一半(加减符号目标数量), 当前1(负号)的数量
int n, half, countt;
// 当前方案书, 各层答案
int sum, ans[N];
void BackTrack(int t)
{
// 到此-个数或+个数已超过目标个数(half),可行性剪枝
if (countt > half || (t * (t - 1) / 2 - countt > half))
return;
// 到达边界,更新sum
if (t > n)
{
sum++;
return;
}
for (int i = 0; i < 2; i++)
{
// 第一层最右边(第t个)可能增加-或+
p[1][t] = i;
countt += i;
for (int j = 2; j <= t; j++)
{
// 向2 ~ t层增加最右边一行(第t - j + 1个)
// 其值是上方([j - 1, t - j + 1]符号和右上[j - 1, t - j + 2]符号的异或
p[j][t - j + 1] = p[j - 1][t - j + 1] ^ p[j - 1][t - j + 2];
countt += p[j][t - j + 1];
}
// 下一层
BackTrack(t + 1);
// 还原countt的计数
for (int j = 2; j <= t; j++)
countt -= p[j][t - j + 1];
countt -= i;
}
}
int main()
{
for (n = 1; n <= 24; n++)
{
countt = 0;
sum = 0;
if (n * (n + 1) / 2 % 2)
{
// 总符号数为奇数,肯定不会满足条件
ans[n] = 0;
continue;
}
half = n * (n + 1) / 4;
BackTrack(1);
ans[n] = sum;
}
for (int i = 0; i <= 24; i++)
{
printf("%d, ", ans[i]);
}
system("pause");
return 0;
}
*/
4. n后问题
/* ************* N Queen (HDU2553) - BackTrack ************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
**********************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 15;
bool Place(int k);
void BackTrack1(int t); // n叉树
void BackTrack2(int t); // 排列树
// 棋盘边长, 当前解数, 记录方案, 搜过的答案
int n, sum, x[N], ans[N];
// 该答案是否被搜过
bool f[N];
int main()
{
while (scanf("%d", &n) && n)
{
if (!f[n])
{
// 没求解过的答案才去求解
sum = 0;
// 如果使用排列树求法需要初始化x[]
BackTrack1(1);
// 记录答案并标记
ans[n] = sum;
f[n] = true;
}
printf("%d\n", ans[n]);
}
system("pause");
return 0;
}
bool Place(int k)
{
for (int j = 1; j < k; j++)
{
if (abs(k - j) == abs(x[j] - x[k]) || x[j] == x[k])
return false;
}
return true;
}
void BackTrack1(int t)
{
// * 解空间结构为n叉树
if (t > n)
{
sum++;
return;
}
for (int i = 1; i <= n; i++)
{
x[t] = i;
if (Place(t))
BackTrack1(t + 1);
}
}
void BackTrack2(int t)
{
// * 排列树解法
// 使用排列树求解前,需要将x[]初始化为x[i] = i
if (t > n)
{
sum++;
return;
}
for (int i = t; i <= n; i++)
{
swap(x[t], x[i]);
if (Place(t))
BackTrack2(t + 1);
swap(x[t], x[i]);
}
}
5. 0-1背包问题
/* ************ Zero-one Pack(P1048) - BackTrack ************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
*************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 150;
struct node
{
// 物品价值, 物品重量
int v, w;
};
bool cmp(const node &x, const node &y)
{
// 物品单位重量价值递减
return x.v * 1.0 / x.w > y.v * 1.0 / y.w;
}
double bound(int i);
void Backtrack(int i);
// 物品数, 背包容量, 当前最优解
int n, c, bestp;
// 背包剩余容量, 当前存放物品价值
int cw, cp;
// 物品信息
node obj[N];
int main()
{
// 输入
cin >> c >> n;
for (int i = 1; i <= n; i++)
cin >> obj[i].w >> obj[i].v;
// 搜索顺序优化, 先放单位价值高的
sort(obj + 1, obj + 1 + n, cmp);
Backtrack(1);
cout << bestp;
system("pause");
return 0;
}
double bound(int i)
{
// 按照优先存放单位价值大的原则填满剩余空间
// 这样得到的最优解大于等于且近似于实际最优解
int leftw = c - cw;
int b = cp;
while (i <= n && obj[i].w <= leftw)
{
// 按照单位价值递减顺序填满剩余空间
// 当所有物品放完, 或下一个物品重量超出剩余空间跳出循环
leftw -= obj[i].w;
b += obj[i].v;
i++;
}
if (i <= n)
{
// 如果背包还有空间, 填满缝隙
// (实际上不可以, 在此用于产生大于等于且近似于实际最有解)
b += obj[i].v * 1.0 / obj[i].w * leftw;
}
return b;
}
void Backtrack(int i)
{
if (i > n)
{
// 到达边界, 更新答案
bestp = cp;
return;
}
if (cw + obj[i].w <= c)
{
// 可行性优化: 放入不超重
cw += obj[i].w;
cp += obj[i].v;
Backtrack(i + 1);
cw -= obj[i].w;
cp -= obj[i].v;
}
// 最优性优化: 不放当前物品, 也有更优的可能性
if (bestp < bound(i + 1))
Backtrack(i + 1);
}
6. 最大团问题
/* *********** Maximum Clique(HDU1530) - BackTrack **********
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
*************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 100;
void scanData();
void BackTrack(int dep);
bool check(int num);
// 图中结点数, 图的邻接矩阵, 最大团结点数, 当前结点数
int n, a[N][N], bestn, cn;
// 最大团, 当前正在讨论中的最大团
bool bestx[N], x[N];
int main()
{
while (scanf("%d", &n) && n)
{
scanData();
BackTrack(1);
printf("%d\n", bestn);
}
system("pause");
return 0;
}
void scanData()
{
bestn = 0;
cn = 0;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
scanf("%d", &a[i][j]);
}
}
}
void BackTrack(int dep)
{
if (dep > n)
{
memcpy(bestx, x, sizeof(bool) * (n + 1));
bestn = cn;
return;
}
bool ok = true; // 可行性剪枝标记
for (int i = 1; i < dep; i++)
{
if (x[i] && !a[dep][i])
{
ok = false;
break;
}
}
if (ok)
{
x[dep] = true;
cn++;
BackTrack(dep + 1);
cn--;
x[dep] = false;
}
if (cn + n - dep > bestn)
{
x[dep] = false;
BackTrack(dep + 1);
}
}
7. 图的m着色问题
/* ********** Coloring Problem(P2819) - BackTrack ***********
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
*************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 100;
void scanData();
bool check(int col);
void BakcTrack(int dep);
// 结点数, 边数, 方案数, 染色方案, 颜色数
int n, k, sum, color[N], m;
// 图的邻接矩阵
bool g[N][N];
int main()
{
scanData();
BakcTrack(1);
cout << sum << endl;
system("pause");
return 0;
}
void scanData()
{
cin >> n >> k >> m;
for (int i = 1; i <= k; i++)
{
int s, e;
cin >> s >> e;
g[s][e] = true;
g[e][s] = true;
}
}
bool check(int dep)
{
// 检查当前染色是否合法
for (int i = 1; i < dep; i++)
{
// 与相邻点颜色相同,返回false
if (g[dep][i] && color[dep] == color[i])
return false;
}
return true;
}
void BakcTrack(int dep)
{
if (dep > n)
{
// 到达边界,方案数++
sum++;
return;
}
for (int i = 1; i <= m; i++)
{
color[dep] = i;
if (check(dep))
{
// 当前颜色合法才继续
BakcTrack(dep + 1);
}
}
}
8. 旅行售货商问题
/* ******************** TSP - BackTrack *********************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 100;
const int INF = 0x3f3f3f3f;
// 结点数, 当前策略, 图, 当前花费
int n, x[N], g[N][N], cc;
// 最优花费, 最优策略
int bestc = INF, bestx[N];
int main()
{
// Get_Data
// BackTrack(1)
// Print_Ans
system("pause");
return 0;
}
void BackTrack(int t)
{
if (t == n)
{
// 递归边界
// 最后一个结点不需选择,剩下的那个就是,但需要满足可行性和最优性
// 可行性:最优一个结点x[n]与前一个结点x[n - 1]和第一个结点x[1]都有路
// 最优性:cc + dis(n-1, n) + dis(n, 1) < bestc
if (g[x[n - 1]][x[n]] && g[x[n]][1] && cc + g[x[n - 1]][x[n]] + g[x[n]][1] < bestc)
{
// refresh ans
memcpy(bestx, x, sizeof(int) * (n + 1));
bestc = cc + cc + g[x[n - 1]][x[n]] + g[x[n]][1];
}
}
else
{
for (int i = t; i <= n;i++)
{
// 上一个结点到当前结点有路且满足最优性剪枝
// 排列树
if (g[x[t - 1]][x[i]] && (cc + g[x[t - 1]][x[i]]) < bestc)
{
swap(x[t], x[i]);
cc += g[x[t - 1]][x[t]];
BackTrack(t + 1);
cc -= g[x[t - 1]][x[i]];
swap(x[t], x[i]);
}
}
}
}
9. 圆排列问题
/* ************ Circle Arrangement - BackTrack **************
*@Author: Pihai Sun *
*@University: Qingdao University *
*@College: College of Computer Science and Technology *
************************************************************/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
const int N = 1000;
void BackTrack(int t);
float Center(int t);
void Compute();
// 圆的总数
int n;
// 当前圆排列圆心横坐标, 当前圆排列
float x[N], r[N];
float minans;
int main()
{
// Get_Data
// BackTrack(1)
// Print_Ans
system("pause");
return 0;
}
void BackTrack(int t)
{
if (t > n)
{
Compute();
return;
}
for (int i = t; i <= n; i++)
{
swap(r[t], r[i]);
float centerx = Center(t);
if (centerx + r[t] + r[1] < minans)
{
x[t] = centerx;
BackTrack(t + 1);
}
swap(r[t], r[i]);
}
}
float Center(int t)
{
// 计算圆在当前圆排列中的横坐标
float valuex, temp = 0;
for (int j = 1; j < t; j++)
{
valuex = x[j] + 2.0 * sqrt(r[t] * r[j]);
temp = max(valuex, temp);
}
return temp;
}
void Compute()
{
// 计算当前圆排列的长度
float low, high;
low = high = 0;
for (int i = 1; i <= n; i++)
{
low = min(low, x[i] - r[i]);
high = max(high, x[i] + r[i]);
}
minans = min(minans, high - low);
}