d f s dfs dfs虽然作为暴力做法的代表,但是很多算法都是可以从 d f s dfs dfs找到影子,比如让很多人头疼的 d p dp dp
这几天做了很多 d p dp dp的问题,想对于从 d f s dfs dfs的方面理解 d p dp dp问题的有些认识写下来,这样我才能离我的目标更进一步。
d p dp dp问题实际上其实是上能解决大多数组合问题,组合问题是指选取无顺序性的一组组合问题,与之相对的是排列问题,排列问题一般很难用 d p dp dp来解决,当然对于组合问题有很多解法,比如:贪心, d p dp dp,分治,而我们的目标就是选出最优的一些数, 比如背包问题就是在若干件物品中选择某些物品来达到最优解,还有数字三角形问题,还有各种组合问题。
我们如果要枚举组合问题每一个方案,然后再对比方案最之间的值,然后寻求最大值,时间复杂度将会达到
2
n
2^n
2n,就算进行非常高效的剪枝,这也将是一个非常高的时间复杂度(当然一般剪枝之后的复杂度一般难以预估,但这是非常不稳定的优化方式,可能对于
<
<
<来说加一个
=
=
=变成
<
=
<=
<=运行时间就会大不相同,我就不知多少次这样被TLE过。),其中有一种特别的dfs方式叫做记忆化搜索,其中优化方式就是
将dfs中重复计算的一些数值保存下来,这个就和dp的思路有点相像了,因为dp也是保存了某些数值,这样说的话有点抽象,来举个经典的例子,01背包问题。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m, f[N][N], v[N], w[N];
int dfs(int m, int cnt)
{
if (f[cnt][m] >= 0) return f[cnt][m];
if (cnt == 0) return 0;
if (m >= v[cnt])return f[cnt][m] = max(dfs(m - v[cnt], cnt - 1) + w[cnt], dfs(m, cnt - 1));
else return f[cnt][m] = dfs(m, cnt - 1);
}
int main()
{
cin >> n >> m;
memset(f, -1, sizeof f);
for (int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i];
dfs(m, i);
}
cout << f[n][m] << endl;
return 0;
}
这个就是01 背包问题的记忆化搜索代码,大家可以看到,每加入一个点,就dfs一次,而dfs过程只用到前边的两种状态,对应着,选与不选,这个虽然是dfs,但是时间复杂度并不是指数级别的时间复杂度,对应着我们dp的状态转移过程。
这个是dp的代码。
int n, m; cin >> n >> m;
for(int i = 1; i <= n ; i ++)
{
int v, w; cin >> v >> w;
for(int j = 0; j <= m; j ++)
{
f[i][j] = f[i - 1][j];
if(j >= v) f[i][j] = max(f[i - 1][j - v] + w, f[i][j]);
}
}
我们先开始看记忆化搜索,观察每种方案的枚举过程,假如我们的 d f s dfs dfs加入一个新点并重新开始的时候, d f s dfs dfs下次就重新开始枚举的时候一定还是按照之前的最优的方案枚举回去,只要枚举到的点对我们新加入的点没有影响,那么 d f s dfs dfs肯定是按照之前的最优的解法枚举回去,而dp对其的优化方式就是保存前边枚举的最优方式,即每一个新点加入进来的时候只需要将前边影响其价值体现的点都枚举一遍对其的决策,然后得出最大值,于是便有了我们的状态转移方程, d p dp dp状态转移的过程就是 d f s dfs dfs枚举一个新加入的点的过程。
仔细看看的这两份代码差不多就能知道这其中的原理,可能这样对于实际解题并没有多大用处,但是理解这个对做题的思路有很大帮助。