题目、示例分析
已知:
n \rm n n 个元件,两个元件间的布线数量 c o n n [ i ] [ j ] \rm conn[\ i\ ][\ j\ ] conn[ i ][ j ],两个元件在线路板的位置分别为 r 、 s r、s r、s,则距离为 d i s t ( r , s ) = a b s ( r − s ) \rm dist(r,s)=abs(r-s) dist(r,s)=abs(r−s),
布线成本 c o s t = ∑ 1 ⩽ i ⩽ j ⩽ n c o n n [ i ] [ j ] × d i s t ( r , s ) \large \rm cost=\displaystyle\sum_{1\leqslant i\leqslant j\leqslant n}conn[i][j]\times dist(r,s) cost=1⩽i⩽j⩽n∑conn[i][j]×dist(r,s)
要求:
① 最少费用 m i n _ c o s t \rm min\_cost min_cost
② 对应的布线方案,即给出元件在布线板上的一个排列
约束:
如果能够想到布线方案就是元件的一个排列,那么约束其实就是排列的约束
进行排列选择时,看当前手中可以用的数字有哪些,把这些数字放在目前的位置上构成一个新排列,一旦这样做,该数字就从手中消失
也就是说,之前已经排列好的数字不能再使用,只能用手中还没用过的数字
这将影响到回溯中 选择部分 的代码,具体来说是循环的起始点 A A A
for i <- A to B:
...
backTrack()
...
示例
- 输入部分:
① 3,说明有 3 个元件,编号记为1、2、3
② 输入conn,这里的输入用了对称矩阵压缩
对于当前编号为 i i i 的元件,之前 1 ∼ i − 1 1\sim i-1 1∼i−1 的元件已经给出了和 i i i 的布线数量,所以不输入
因此 第 i i i 行输入的: i + 1 ∼ n i + 1\sim n i+1∼n 的元件和 i i i 的布线数量,由示例可得:
或者说,元件记为节点,边表示布线,边的权重表示布线数量
则布线数量表示为无向图
2 | 3 | |
---|---|---|
1 | 1 − 2 、 2 − 1 1-2、2-1 1−2、2−1的布线数量:2 | 1 − 3 、 3 − 1 1-3、3-1 1−3、3−1的布线数量:3 |
2 | —— | 2 − 3 、 3 − 2 2-3、3-2 2−3、3−2的布线数量:3 |
- 输出部分:
① 最少费用:10
② 最优排列:①、③、②
可以计算出各个元件之间的
d
i
s
t
\rm dist
dist,元件① 到 元件③ 距离为 1,① 到 ② 为 2,③ 到 ② 为 1,因此
c
o
s
t
=
1
∗
c
o
n
n
[
1
]
[
3
]
+
2
∗
c
o
n
n
[
1
]
[
2
]
+
1
∗
c
o
n
n
[
2
]
[
3
]
=
1
∗
3
+
2
∗
2
+
1
∗
3
=
10
\begin{gather}\rm cost &=& \rm 1*conn[1][3]+2*conn[1][2]+1*conn[2][3]\\ &=& \rm 1*3+2*2+1*3\quad=\quad10\end{gather}
cost==1∗conn[1][3]+2∗conn[1][2]+1∗conn[2][3]1∗3+2∗2+1∗3=10
回溯法思路
代码套路
回溯法类似于DFS搜索的思想
站在当前的位置上,有很多选择 OP1、OP2、OP3……
思考做出每一个选择之后再做出选择(递归……),最终的结果如何
从中选择最优的选择 OP
def backTrack(visited, Option): # visited记录过去做了什么选择,Option是所有选择
# 终止条件
if base case:
# 可能剪枝(如果可以)
# 根据情况操作
return
# 可能剪枝(如果可以)
# 对于所有选择,遍历考虑结果如何
for i in Option:
# 可能剪枝(如果可以)
remove i # 做出选择
visited.add(i) # 可选,记录路径
backTrack(visited, Option)
visited.pop(i) # 可选,撤销路径
recover i # 撤销选择
填充
按照上面的套路分析:
1. 终止条件是什么,做什么?
排列的时候,当手中没牌的时候排列结束
手里没牌说明第 n n n 张牌也在队列中,如果用一个步数计数器 s t e p step step 来表示回溯树的层,那么 s t e p = = n step ==n step==n 的时候说明结束
到达根节点的时候,需要查看是否要更新 m i n _ c o s t \rm min\_cost min_cost 和 排列方案,依据是当前排列的 c o s t \rm cost cost 更小,代码如下:
if (step == n) {
// 求当前排列的费用
int cur_cost = costCalculate(n);
if (cur_cost < min_cost) {
min_cost = cur_cost;
// 更新路线
// .......
}
}
2. 选择是什么?
查看手中的牌(元件),选择一个放到当前位置(布线板)上,当前位置即回溯树的层数 s t e p step step
按照排列的思路,得到一个新排列可以将两个数的位置交换,所以代码的思路就是将当前位置的元件和手中的某个元件交换位置
for (int i = step; i <= n; ++i) {
// 选择 i 来替换step位置的元件
swap(step, i);
backTrack();
// 回溯,撤销选择
swap(step, i);
}
数据结构、函数设计
1. 元件序列、布线数量矩阵、费用
元件序列使用 a r r a n g e [ ] \rm arrange[\ ] arrange[ ] 数组保存,值初始化为下标,表示每个元件起初都在原位置的排列
最优排列使用 a n s [ ] \rm ans[\ ] ans[ ] 数组保存
布线数量矩阵使用 c o n n [ ] [ ] \rm conn[\ ][\ ] conn[ ][ ] 数组保存,根据题目条件 n ⩽ 20 n\leqslant 20 n⩽20,可以初始化为 25 行、25 列
最小费用用 m i n _ c o s t \rm min\_cost min_cost 保存
2. 计算费用函数
当 s t e p = = n \rm step == n step==n 时,首先要计算当前方案的费用,然后进行比较看看能不能更新
根据上面定义的数组、变量,以及计算方法,可得代码:
int costCalculate(int number) {
int sum = 0;
// 元件编号从 1 开始
for (int i = 1; i <= number; ++i) {
for (int j = 1; j <= number; ++j) {
// distance = abs(arrange[i] - arrange[j]);
sum += cnn[i][j] * abs(arrange[i] - arrange[j]);
}
}
return sum;
}
3. 主函数
① 输入元件个数,初始化序列数组
② 初始化 c n n [ ] [ ] \rm cnn[\ ][\ ] cnn[ ][ ] 布线数量函数
③ 回溯函数, s t e p = = 1 \rm step==1 step==1 开始(元件编号从 1 开始)
④ 输出费用、方案
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// min_cost 初始化为较大的值
int n, min_cost = 0x3f3f3f3f;
int conn[25][25];
vector<int> arrange, ans;
int costCalculate() {
int sum = 0;
// 元件编号从 1 开始
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
sum += conn[i][j] * abs(arrange[i] - arrange[j]);
return sum;
}
void backTrack(int step) {
if (step == n) {
int cur_cost = costCalculate();
// 费用更小,更新
if (cur_cost < min_cost) {
min_cost = cur_cost;
// 更新最优序列
for (int i = 1; i <= n; ++i)
ans[i] = arrange[i];
}
}
// 排列选择
for (int i = step; i <= n; ++i) {
// 注:交换的不是下标,而是排列数组的值
swap(arrange[step], arrange[i]);
// 下一个位置
backTrack(step + 1);
swap(arrange[step], arrange[i]);
}
}
int main()
{
scanf("%d", &n);
// 初始化排列数组
arrange.resize(n + 1);
ans.resize(n + 1);
for (int i = 1; i <= n; ++i)
arrange[i] = i;
for (int i = 1; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
scanf("%d", &conn[i][j]);
}
}
backTrack(1);
printf("%d\n", min_cost);
for (int i = 1; i <= n; ++i)
printf("%d ", ans[i]);
}