DFS
0x01 剪枝
Part1.剪枝方法
\(DFS\) 是一种常见的算法,大部分情况下,很少会爆搜为正解的题目。因为 \(DFS\) 的时间复杂度特别高。
我们可以先写一段 dfs 的伪代码
int ans = 最坏情况, now; // now 为当前答案
void dfs(传入数值)
{
if (到达目的地)
{
ans = 从当前解与已有解中选最优;
}
for (遍历所有可能性)
{
if (可行)
{
进行操作;
dfs(缩小规模);
撤回操作;
}
}
}
记忆化搜索
因为在搜索中,相同的传入值往往会带来相同的解,那我们就可以用数组来记忆
int g[N]; // 定义记忆化数组
int ans = 最坏情况, now;
void dfs(传入数值)
{
if (g[规模] != 无效数值) return; // 或记录解,视情况而定
if (到达目的地) ans = 从当前解与已有解中选最优; // 输出解,视情况而定
for (遍历所有可能性)
{
if (可行)
{
进行操作;
dfs(缩小规模);
撤回操作;
}
}
}
int main()
{
// ...
memset(g, 无效数值, sizeof(g)); // 初始化记忆化数组
// ...
}
最优性剪枝
在搜索中导致运行慢的原因还有一种,就是在当前解已经比已有解差时仍然在搜索,那么我们只需要判断一下当前解是否已经差于已有解。
int ans = 最坏情况, now;
void dfs(传入数值)
{
if (now比ans的答案还要差) return;
if (到达目的地) ans = 从当前解与已有解中选最优;
for (遍历所有可能性)
{
if (可行)
{
进行操作;
dfs(缩小规模);
撤回操作;
}
}
}
可行性剪枝
在搜索过程中当前解已经不可用了还继续搜索下去也是运行慢的原因。
int ans = 最坏情况, now;
void dfs(传入数值)
{
if (当前解已不可用) return;
if (到达目的地) ans = 从当前解与已有解中选最优;
for (遍历所有可能性)
{
if (可行)
{
进行操作;
dfs(缩小规模);
撤回操作;
}
}
}
Part2.剪枝思路
剪枝思路有很多种,大多需要对于具体问题来分析,在此简要介绍几种常见的剪枝思路。
-
极端法:考虑极端情况,如果最极端(最理想)的情况都无法满足,那么肯定实际情况搜出来的结果不会更优了。
-
调整法:通过对子树的比较剪掉重复子树和明显不是最有「前途」的子树。
-
数学方法:比如在图论中借助连通分量,数论中借助模方程的分析,借助不等式的放缩来估计下界等等。
Part3.例题
[NOIP2002 普及组] 选数
题目传送门
此题并不需要剪枝,注释代码
#include <bits/stdc++.h>
#define int long long
#define rint register int
#define endl '\n'
using namespace std;
const int N = 2e1 + 5;
int n, k;
int a[N];
int ans;
bool isprime(int p)
{
for (rint i = 2; i * i <= p; i++)
{
if (p % i == 0)
{
return false;
}
}
return true;
}
void dfs(int cnt, int sum, int start)
//cnt 代表现在选择了多少个数
//sum 表示当前的和
//start 表示从第几个数开始
{
if (cnt == k)
{
if (isprime(sum))
{
ans++;
}
return ;
}
for (rint i = start; i <= n; i++)
{
dfs(cnt + 1, sum + a[i], i + 1);
}
}
signed main()
{
cin >> n >> k;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
}
dfs(0, 0 ,1);
cout << ans << endl;
return 0;
}
小木棍
题目传送门
非常经典的剪枝题。
- 可行性剪枝:记录上一次失败的值, 如果这次还是的话, 那么肯定是不能选择的
- 最优性剪枝:比 maxx 小的一定不是最优,所以从 maxx 开始搜索
其他剪枝见注释代码
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e2 + 5;
int n;
int a[N];
bool v[N];
int len, cnt;
int sum;
int maxx;
bool dfs(int x, int now, int last)
// x 为当前第几段了
//now 记录当前段的长度
//last 为上一次已经选过的值
{
if (x > cnt)//如果当前搜到的段数已经在 cnt 之后,说明可行
{
return 1;
}
if (now == len)//如果长度刚刚好,直接下一次搜索
{
return dfs(x + 1, 0, 1);
}
int fail = 0;
//记录上一次失败的值,如果这次还是的话,那么肯定是不能选择的
for (rint i = last; i <= n; i++)
{
if (!v[i] && now + a[i] <= len && fail != a[i])
{
v[i] = 1;
if (dfs(x, now + a[i], i + 1))
{
return 1;
}
fail = a[i];
v[i] = 0;
if (now == 0 || now + a[i] == len)
{
return 0;
}
}
}
return 0;
}
int main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
sum += a[i];
maxx = max(maxx, a[i]);
//找出最大值和所有数的和
}
sort(a + 1, a + n + 1);
reverse(a + 1, a + n + 1);//从大到小排序
for (len = maxx; len < sum; len++)
//比maxx小的一定不是最优,所以从maxx开始搜索
{
if (sum % len)
//一定分不出来
{
continue;
}
cnt = sum / len;
memset(v, 0, sizeof v);
if (dfs(1, 0, 1))
{
break;
}
}
cout << len << endl;
return 0;
}
[NOI1999] 生日蛋糕
此题更多的是数学上的剪枝 + 贪心
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
const int inf = 1e9;
int n, m;
int ans = inf;
int mins[N], minv[N];
void dfs(int c, int v, int s, int h, int r)
//c 层数, v 体积, s 抹奶油的外表面积 , r 半径, h 高度
{
if(c == 0)
{
if(v == n)
{
ans = min(ans ,s);
}
return;
}
if(v + minv[c] > n) return;
/*
当前的体积 > n return
*/
if(s + mins[c] > ans) return;
if(s + 2 * (n - v) / r > ans) return;
/*
当前的奶油面积 + 之后的奶油面积 > 现在已求出的的最小奶油面积 return
*/
for (rint i = r - 1; i >= c; i--)
{
if(c == m)
{
s = i * i;
}
int Maxh = min(h - 1, (n - v - minv[c - 1]) / (i * i));
for (rint j = Maxh; j >= c; j--)
{
dfs(c - 1, v + i * i * j, s + 2 * i * j, j, i);
}
}
}
signed main()
{
cin >> n >> m;
int MaxR = sqrt(n);
for(int i = 1; i <= n; i++)
{
minv[i] = minv[i-1] + i * i * i;
mins[i] = mins[i-1] + 2 * i * i;
}
dfs(m, 0, 0, n, MaxR);
if(ans == inf)
{
ans = 0;
}
cout << ans << endl;
return 0;
}
AcWing.165 小猫爬山
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int N = 2e1 + 5;
const int inf = 1e18;
int n, w;
int a[N];
int res = inf;
int p[N];
//p[i] 表示的是第 i 个车已经装了重量为 p[i] 的猫
void dfs(int x, int cnt)
//该拿第 x 只猫了
//累计用了 cnt 个车
{
if (cnt >= res)
{
return ;
}
if (x == n + 1)
{
res = min(res, cnt);
return ;
}
for (rint i = 1; i <= cnt; i++)
{
if (a[x] <= w - p[i])
//如果还能放小猫
{
p[i] += a[x];
dfs(x + 1, cnt);
p[i] -= a[x];//恢复现场
}
}
p[++cnt] = a[x];
dfs(x + 1, cnt);
p[cnt] = 0;//恢复现场
}
signed main()
{
cin >> n >> w;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
}
sort(a + 1, a + n + 1);
reverse(a + 1, a + n + 1);
dfs(1, 0);
cout << res << endl;
return 0;
}
0x02 迭代加深
Part1.算法思路
迭代加深是一种 每次限制搜索深度的 深度优先搜索。
迭代加深搜索的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度 depth
,当 depth
达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。
既然是为了找最优解,为什么不用 BFS 呢?我们知道 BFS 的基础是一个队列,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,使用队列的 BFS 就显出了劣势。事实上,迭代加深就类似于用 DFS 方式实现的 BFS,它的空间复杂度相对较小。
当搜索树的分支比较多时,每增加一层的搜索复杂度会出现指数级爆炸式增长,这时前面重复进行的部分所带来的复杂度几乎可以忽略,这也就是为什么迭代加深是可以近似看成 BFS 的。
Part2.例题
UVA529 Addition Chains
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e4 + 5;
int n, path[N];
bool dfs(int u, int k)
//u 当前深度
//k 最大深度
{
if (path[u - 1] > n)
{
return 0;
}
if (u == k)
{
return path[u - 1] == n;
}
if (path[u - 1] * ((long long)1 << (k - u)) < n)
//后面每一项最多是前一项的2倍
//如果没有这个剪枝,仍然可以通过 AcWing.170,但无法通过此题
{
return 0;
}
bool v[N] = {0};
for (rint i = u - 1; i >= 0; i--)
{
for (rint j = i; j >= 0; j--)
{
int s = path[i] + path[j];
if (s > n || s <= path[u - 1] || v[s])
{
continue;
}
v[s] = 1;
path[u] = s;
if (dfs(u + 1, k))
{
return 1;
}
}
}
return 0;
}
signed main()
{
path[0] = 1;
while (cin >> n and n != 0)
{
int depth = 1;
while (!dfs(1, depth))
{
depth++;
}//迭代加深精髓
for (rint i = 0; i < depth; i++)
{
cout << path[i] << ' ';
}
puts("");
}
return 0;
}
0x03 折半搜索
Part1.算法思路
折半搜索跟双向搜索具体有什么区别,或者说他们两个本身就是一个算法什么的我也分不清,那就不分了,因为它们思想的本质是一样的。
算法本质:以空间换时间
这句话是什么意思呢?假设现在有一个 01 选择问题,对于每一个物品有选和不选两个方案,我们要对 \(n\) 个物品找出所有方案,复杂度为 \(2^n\) 对吧,如果 \(n\) 到了 \(40\) 直接就炸了,这时可以折半,先搜索前面的,再搜索后面的。
之后如果相查答案直接二分即可,在对后半部分进行搜索时对前面进行查询即可。
Part2.例题
AcWing.171 送礼物
题目传送门
由于 \(W\) 的范围很大,所以显然不可以直接背包,观察数据范围,\(N\) 很小,而且折半后不会超范围。
我们只需要对前一半个重量进行计算,之后再对后一半的数进行计算即可。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 46;
const int M = 1 << ((N >> 1) + 1);
const int inf = 1e18;
int n, m;
int k;
int g[N], weight[M];
int cnt, ans = -inf;
void dfs1(int u, int s)
{
if (u == k + 1)
{
weight[++cnt] = s;
return;
}
dfs1(u + 1, s);// 枚举当前不选这个物品
if (m - g[u] >= s)//如果没超,才有必要搜索
{
dfs1(u + 1, s + g[u]);
}
}
void dfs2(int u, int s)
{
if (u == n + 1)
{
int l = 1, r = cnt;
while (l < r)//二分查找
{
int mid = (l + r + 1) >> 1;
if (weight[mid] <= m - s)
{
l = mid;
}
else
{
r = mid - 1;
}
}
if (weight[l] <= m - s)//更新答案
{
ans = max(ans, weight[l] + s);
}
return ;
}
dfs2(u + 1, s);
if (m - g[u] >= s)
{
dfs2(u + 1, s + g[u]);
}
}
signed main()
{
cin >> m >> n;
for (rint i = 1; i <= n; i++)
{
cin >> g[i];
}
sort(g + 1, g + n + 1);
reverse(g + 1, g + n + 1);
k = n / 2;
cnt = 1;
dfs1(1, 0);
sort(weight + 1, weight + cnt + 1);
cnt = unique(weight, weight + cnt + 1) - weight - 1;
//去重
dfs2(k + 1, 0);
cout << ans << endl;
return 0;
}
[CEOI2015] 世界冰球锦标赛
先将第一个数组排序,然后枚举第二个数组的每一个数,在第一个数组中寻找和大于 \(m\) 的数的位置。
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int N = 2e6 + 5;
int n, m, k;
int a[N], b[N];
int w[N];
int ans;
int cnt1, cnt2;
void dfs1(int x, int sum)
{
if (sum > m)
{
return;
}
if (x > k)
{
a[++cnt1] = sum;
return;
}
dfs1(x + 1, sum + w[x]);
dfs1(x + 1, sum);
}
void dfs2(int x, int sum)
{
if (sum > m)
{
return;
}
if (x > n)
{
b[++cnt2] = sum;
return;
}
dfs2(x + 1, sum + w[x]);
dfs2(x + 1, sum);
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
cin >> w[i];
}
k = n / 2;
dfs1(1, 0);
dfs2(k + 1, 0);
std::sort(b + 1, b + cnt2 + 1);
for (rint i = 1; i <= cnt1; i++)
{
ans += std::upper_bound(b + 1, b + cnt2 + 1, m - a[i]) - (b + 1);
}
cout << ans << endl;
return 0;
}
0x04 IDA*
Part1.算法思路
IDA*其实就是迭代加深外加估价函数,迭代加深前面已经讲到,所以如何实现估价函数。
\(f(n)=g(n)+h(n)\)
其中 \(f(n)\) 是节点的估价函数,\(g(n)\) 是现在的实际步数,\(h(n)\) 是对未来步数的最完美估价(“完美”的意思是可能你现实不可能实现,但你还要拿最优的步数去把$ h(n)$ 算出来,可能不太好口胡,可以参考下面的实例)。
伪代码如下:
void dfs(int dep, int maxdep)
{
if(evaluate() + dep > maxdep) return;
if(!evaluate)
{
success=1;
printf("%d\n",dep);
return;
}
......
}
Part2.例题
[SCOI2005] 骑士精神
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e1 + 5;
const int b[N][N] = {
{0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1},
{0, 0, 1, 1, 1, 1},
{0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0},
}; //最理想的样子
const int dx[] = {1, -1, -1, 1, 2, -2, -2, 2};
const int dy[] = {2, -2, 2, -2, 1, -1, 1, -1};
int a[N][N];
bool flag;
int evaluate()
{
int tot = 0;
for (rint i = 1; i <= 5; i++)
{
for (rint j = 1; j <= 5; j++)
{
if (a[i][j] != b[i][j])
{
tot++;
}
}
}
return tot;
}
bool check(int x, int y)
{
if (x < 1 || x > 5 || y < 1 || y > 5)
{
return 0;
}
return 1;
}
void dfs(int now, int sum, int x, int y)
/*
now 表示当前步数
sum 表示总步数
x, y 表示空格坐标
*/
{
if (now == sum && !evaluate())
{
flag = 1;
return ;
}
for (rint i = 0; i < 8; i++)
{
int xx = x + dx[i];
int yy = y + dy[i];
if (!check(xx, yy))
{
continue;
}
swap(a[xx][yy], a[x][y]);
if (evaluate() + now - 1 < sum)
{
dfs(now + 1, sum, xx, yy);
}
swap(a[xx][yy], a[x][y]);//还原现场
}
}
int main()
{
int T;
cin >> T;
while (T--)
{
int s = 0, t = 0;
flag = 0;
memset(a, 0, sizeof a);
for (rint i = 1; i <= 5; i++)
{
for (rint j = 1; j <= 5; j++)
{
char ch;
cin >> ch;
if (ch == '*')
{
a[i][j] = 2;
s = i;
t = j;
}
else
{
a[i][j] = ch - 48;
}
}
}
if (!evaluate())
{
puts("0");
continue;
}
for (rint i = 1; i <= 15; i++)
{
dfs(0, i, s, t);
if (flag == 1)
{
cout << i << endl;
break;
}
}
if (!flag)
{
puts("-1");
}
}
return 0;
}
UVA11212 编辑书稿
设 $f(x)=g(x)+d(x)$
其中 $f(x)$ 为总次数, $g(x)$ 为当前次数, $d(x)$ 为剩下次数。
设 $h(x)$ 为后继不正确的数的数
因为每一次移动最多可以将三个不正确的数改为正确的
则 $d(x)\ge h(x)/3$
则 $f(x)\ge g(x)+h(x)/3$
所以 $3 * f(x)\ge 3*g(x)+h(x)$
所以最后估价函数返回值要调到 $(cnt + 2) / 3$#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int N = 1e1 + 5;
int n;
int a[N], w[N][N];
int times;
int evaluate()
{
int cnt = 0;
for (rint i = 0; i + 1 < n; i++)
{
if (a[i + 1] != a[i] + 1)
{
cnt++;
}
}
return (cnt + 2) / 3;
}
bool check()
{
for (rint i = 0; i + 1 < n; i++)
{
if (a[i + 1] != a[i] + 1)
{
return 0;
}
}
return 1;
}
bool dfs(int u, int dep)
{
if (u + evaluate() > dep)
{
return 0;
}
if (check())
{
return 1;
}
for (rint len = 1; len <= n; len++)
{
for (rint i = 0; i + len - 1 < n; i++)
{
int j = i + len - 1;
for (rint k = j + 1; k < n; k++)
{
memcpy(w[u], a, sizeof a);
int x, y;
for (x = j + 1, y = i; x <= k; x++, y++)
{
a[y] = w[u][x];
}
for (x = i; x <= j; x++, y++)
{
a[y] = w[u][x];
}
if (dfs(u + 1, dep))
{
return 1;
}
memcpy(a, w[u], sizeof a);
//还原现场
}
}
}
return 0;
}
signed main()
{
while (cin >> n and n != 0)
{
for (rint i = 0; i < n; i++)
{
cin >> a[i];
}
int depth = 0;
while (!dfs(0, depth))
{
depth ++ ;
}
cout << "Case " << ++times << ": " << depth << endl;
}
return 0;
}