# DFS
众所周知,搜索和 DP 是不分家的,几乎所有的 DP 都可以转化为搜索。当想不出正解时,DFS 也是骗分的好手段。
主要的搜索手段有:
- DFS/BFS 爆搜
- 双向 BFS
- 启发式搜索(又称 A*)
- 迭代加深搜索
- IDA*(迭代加深 + 启发式)
- 记忆化搜索
- 剪枝
重要程度:1,7,6 - 4,3 - 5,2。
下面这几道题都是去年当时不会做的,结果一年过去了,还是不会,呜呜呜
# P3956 棋盘
NOIp 2017 普及
搜索裸题
显然不可以用 vis [] 来判断,因此会搜到重复的状态,考虑记忆化搜索。
网上的题解好像都没有正确性证明,可能觉得太显然了?可我就在这里卡了好久啊 /kk
- 如果当前格子本来就有颜色,那么魔法一定可用
- 如果当前格子原本没有颜色,那么只要搜到这个格子,魔法其实只有一种情况就是不可用
而且记搜的时候注意剪枝条件一定要 ** 写到上界!** 比如下面这段代码, f >= mem[x][y]
如果改成 f > mem[x][y]
,100 直接变 70……
#include <bits/stdc++.h> | |
using namespace std; | |
const int N = 110; | |
int m, n, c[N][N], mem[N][N]; | |
int d[4][2] = <!--swig0-->; | |
int ans = 0x3f3f3f3f; | |
void dfs(int x, int y, int f, int cc) { | |
//f: 代价 | |
//cc != -1 不能用魔法,此处改成的颜色 | |
//cc == -1 可以 | |
if(f >= mem[x][y] || f >= mem[m][m]) return; | |
mem[x][y] = f; | |
if(x == m && y == m) return; | |
for(int i=0; i<4; ++i) { | |
int xx=x+d[i][0],yy=y+d[i][1]; | |
if(xx<1||xx>m||yy<1||yy>m) continue; | |
if(cc!=-1&&c[xx][yy]==cc||cc==-1&&c[xx][yy]==c[x][y]) { | |
dfs(xx, yy, f, -1); | |
} else if(cc!=-1&&c[xx][yy]!=cc&&c[xx][yy]!=-1||cc==-1&&c[xx][yy]!=c[x][y]&&c[xx][yy]!=-1) { | |
dfs(xx, yy, f+1, -1); | |
} else if(cc==-1&&c[xx][yy]==-1) { | |
dfs(xx, yy, f+2, c[x][y]); | |
} | |
} | |
} | |
int main() { | |
scanf("%d%d", &m, &n); | |
memset(c, -1, sizeof c); | |
memset(mem, 0x3f, sizeof mem); | |
for(int i=1; i<=n; ++i) { | |
int x, y, cc; scanf("%d%d%d", &x, &y, &cc); | |
c[x][y] = cc; | |
} | |
dfs(1, 1, 0, -1); | |
printf("%d\n", mem[m][m] == 0x3f3f3f3f ? -1 : mem[m][m]); | |
return 0; | |
} |
# P3959 宝藏
NOIp 2017 提高
去年做的时候就连 70 分做法都不会。一年过去了,还是不会,呜呜呜
(下面为自己开脱)
这道题的部分分思想其实还挺重要的。
我们知道,一般的 DFS 就是在找出很多单条路径的过程中,获得最优答案。形式也很固定,一般形如:
void/int dfs(int u, int sum, int cnt, ...) |
而这道题不然。
从样例二就可以看出,最终合法方案呈树形,而不是单条路径。一上来可能会想到生成树,但是假掉了。、
反例:
这张图从 1 号点开始的 Prim 便是错的。
如果跑 Prim,从 1 号点开始的话,我们会先访问 2,(此时花费为 1),然后我们会访问 3,此时花费为 3 * 2,然后由于只有 4 号点未访问,这时 3 号的访问顺序为 3,访问 4 号的代价是 3 * 100 = 300,显然这种做法不是最优的,正确的答案应该是 211(从 1 号点开始的最小花费为 211)。
虽然直接跑 Prim 是错的,但是 Prim 的思想仍然重要,这一思想也在最短路等很多算法中有应用:用更新过的点去更新其他点。
我们设计一个不基于点的 dfs 函数,参数只需传递代价,每次迭代,先枚举访问过的点,再枚举一个未访问且未开通路径的点,进行松弛,将代价持续传递下去。计算代价需要记录点的深度,也要通过父节点传递下去。
代码不长,也不难写,说是白给 70 分,其实思路并不好想(虽然比正解 状压好写多了)。毕竟是紫题嘛,像我这种菜鸡也不打算 AC,能骗多少是多少。
总的来说,这道题的关键还是在于生成树,需要认真看题,熟练运用普及算法(
# Code:
#include <cstdio> | |
#include <cstring> | |
const int INF = 0x7f7f7f7f; | |
int min(int a, int b) { return a < b ? a : b; } | |
int map[20][20]; | |
int dep[20], best, n, m, num; | |
bool vis[20]; | |
void dfs(int x) { | |
if(x >= best) return; // 注意这里的剪枝! | |
if(num == 0) { | |
best = x; | |
return; | |
} | |
for(int i=1; i<=n; ++i) { | |
if(vis[i]) { | |
for(int j=1; j<=n; ++j) { | |
if(!vis[j] and map[i][j] < INF) { | |
vis[j] = true; --num; | |
dep[j] = dep[i] + 1; | |
dfs(x + map[i][j] * dep[j]); | |
vis[j] = false; ++num; | |
} | |
} | |
} | |
} | |
} | |
int main() { | |
memset(map, 0x7f, sizeof map); | |
scanf("%d%d", &n, &m); | |
for(int i=1; i<=m; ++i) { | |
int x, y, v; | |
scanf("%d%d%d", &x, &y, &v); | |
map[x][y] = map[y][x] = min(map[x][y], v); | |
} | |
best = INF; num = n; | |
for(int i=1; i<=n; ++i) { | |
vis[i] = true; --num; | |
dep[i] = 0; | |
dfs(0); | |
vis[i] = false; ++num; | |
} | |
printf("%d\n", best); | |
return 0; | |
} |
# 状压 DP
# 常用操作
设全集为
for(int y = x; y; y = (y-1) & x)
:枚举 x 的每个子集x^y
:x 集合中刨去 yx&y == y
:y 是 x 的子集
# P3052 [USACO12MAR]Cows in a Skyscraper G
# P3959 宝藏
对就是刚才那道题。
令 表示从起点到准备向外发边的点 的距离为 ,准备要把集合 中的点挖通
转移时,枚举从节点 打出的边 i->k,再枚举从 要打通的子集 ,那么
转移顺序一定要想好。
最后取所有 的最小值即可,其中 是全集。
核心代码
// 预处理集合中 1 的个数 | |
for (int i = 1; i < mx; ++i) | |
sz[i] = sz[i & (i - 1)] + 1; | |
// 预处理 lowbit | |
for (int i = 0; i < n; ++i) | |
mn[1 << i] = i; | |
for (int i = 1; i < mx; ++i) | |
mn[i] = mn[i & -i]; | |
for (int i = 0; i < n; ++i) | |
f[n - 1][i][0] = 0; // 初值 | |
for (int j = n - 2; j >= 0; --j) { // 距离 | |
for (int i = 0; i < n; ++i) { // 发边点 | |
f[j][i][0] = 0; // 初值 | |
for (int S = 1; S < (1 << n); ++S) { | |
if (((~S >> i) & 1) //i 不在集合 S 里 | |
&& sz[S] <= n - j - 1) // 不会多挖 | |
for (int S2 = S; S2; S2 = (S2 - 1) & S) { //S 的每个子集 | |
int tmp = S2; | |
for (int k = mn[tmp]; tmp; k = mn[tmp &= ~(1 << k)]) //S2 里的每个点 | |
if (mp[i][k] ^ INF) // 存在待挖的边 | |
f[j][i][S] = min(f[j][i][S], f[j + 1][k][S2 & ~(1 << k)] + f[j][i][S & ~S2] + mp[i][k] * (j + 1)); | |
} | |
} | |
} | |
} | |
int ans = INF; | |
for (int i = 0; i < n; ++i) | |
ans = std::min(ans, f[0][i][(mx - 1) & ~(1 << i)]); |