初步总结一个动态规划做题思路:
- 初始化,或者说边界(很重要,其余状态就是通过初始状态转化过去的
- 状态转移方程(状态计算)
时间复杂度分析:
状态数 * 转移数
背包问题
1. 01 01 01背包:每件物品最多用一次
2.完全背包:每件物品有无限个
3.多重背包:每种物品个数不一样,有限制(可优化)
4.分组背包: n n n组物品,每组物品最多只能选一种物品
例题: 01 01 01背包
优化前:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];//状态集合,初始值都为0
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)//*从第1件物品开始枚举,因为第0件已经为0
for(int j=0;j<=m;j++){/从0开始枚举体积
f[i][j]=f[i-1][j];//体积为j时不选第i件,等价于体积为j,还没选第i件物品
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//*如果目前体积大于等于目前正在选的第i件物品的体积,将选与不选这一件物品获得的价值作比较取最大值
}
cout << f[n][m] << endl;
return 0;
}
优化后:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N];//去掉第一维
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
// for(int i=1;i<=n;i++)
// for(int j=0;j<=m;j++){
// f[i][j]=f[i-1][j];去掉第一维->f[j]=f[j]恒等式直接删掉
// if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);将for循环条件优化为(int j=v[i];j<=m;j++) 后续则变为f[j]=max(f[j],f[j-v[i]]+w[i]); 但此时我们使用的数据不是v[i-1]的数据了,因为j-v[i]一定是小于j,所以f[j-v[i]]会较早更新到,后续就会使用到这一轮刚跟新了的数据,也就是v[i]这一层的数据,但将j从m到v[i]递减枚举就会保证使用的数据全部为v[i-1]层的
// }
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)//*从m到v[i]开始枚举保证使用的是v[i-1]的数据
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout << f[m] << endl;
return 0;
}
对于优化方式的一些解释:
采用滚动数组,在下一次更新前,使用的数据是上一次存在的(今天才悟到滚动数组的含义!)
首先我们可将for循环条件优化为 ( i n t j = v [ i ] ; j ≤ m ; j + + ) (int\ j=v[i];j\le m;j++) (int j=v[i];j≤m;j++) 后续则变为 f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] + w [ i ] ) ; f[j]=max(f[j],f[j-v[i]]+w[i]); f[j]=max(f[j],f[j−v[i]]+w[i]); 但此时我们使用的数据不是 v [ i − 1 ] v[i-1] v[i−1]的数据了,因为 j − v [ i ] j-v[i] j−v[i]一定是小于 j j j,所以 f [ j − v [ i ] ] f[j-v[i]] f[j−v[i]]会较早更新到,后续就会使用到这一轮刚跟新了的数据,也就是 v [ i ] v[i] v[i]这一层的数据,但将 j j j从 m m m到 v [ i ] v[i] v[i]递减枚举就会保证使用的数据全部为 v [ i − 1 ] v[i-1] v[i−1]层的,也就是全都是上一层保存的,未在这一层更新过的。(终于搞懂了)
例题:完全背包问题
优化过程:
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int f1[N];
signed main(){
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
// for(int i=0;i<=n;i++) //* 优化前
// for(int j=0;j<=m;j++)
// for(int k=0;k*v[i]<=j;k++) f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
// cout << f[n][m] << endl;
//* 优化后 变为二维
// for(int i=1;i<=n;i++)
// for(int j=0;j<=m;j++){
// f[i][j]=f[i-1][j];
// if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); //!从i转移过来 因为在这一层上要选若干个,用这一层更新过的数据
// }
// cout << f[n][m] << endl;
//* 优化为一维
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++)//*使用第i层数据,从小到大枚举
f1[j]=max(f1[j],f1[j-v[i]]+w[i]);
cout << f1[m] << endl;
return 0;
}
优化版本:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int f[N],v[N],w[N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout << f[m] << endl;
return 0;
}
优化原理:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Qv9JjNt-1652273545557)(C:\Users\Lenovo\Pictures\Saved Pictures\image-20220418180709459.png)]
在同一层上,依次减掉更多的 v [ i ] v[i] v[i],下一次减 v [ i ] v[i] v[i]可以在上一次的基础上进行操作,所以用的是同一层的数据。
例题:多重背包问题
状态转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − v [ i ] ∗ k ] + w [ i ] ∗ k ) ( 0 ≤ k ≤ s [ i ] ) f[i][j]=max(f[i-1][j-v[i]*k]+w[i]*k)\ (0\le k\le s[i]) f[i][j]=max(f[i−1][j−v[i]∗k]+w[i]∗k) (0≤k≤s[i])
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i] >> s[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
{
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
cout << f[n][m] << endl;
return 0;
}
优化后:
(二进制优化)
#include <bits/stdc++.h>
using namespace std;
const int N =25000,M=2010;
int n,m;
int v[N],w[N];
int f[N];
signed main(){
cin >> n >> m;
int cnt=0;
//for(int i=1;i<=n;i++) cin >> v[i] >> w[i] ;
//* 优化前
// for(int i=1;i<=n;i++)
// for(int j=0;j<=m;j++)
// for(int k=0;k<=s[i]&&k*v[i]<=j;k++){
// f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
// }
// cout << f[n][m] << endl;
//* 优化后(二进制优化) 将s[]拆分 再对分完的组进行01背包问题求解
for(int i=1;i<=n;i++){
int a,b,s;
cin >> a >> b >> s;
int k=1;//*分组后每组数量
while(k<=s){ //*组内数量还小于等于剩余数量再把剩余物品分组
cnt++;//*组数下标
v[cnt]=a*k;//第cnt组的体积
w[cnt]=b*k;//价值
s-=k;//*剩余未打包的数量
k<<=1;//下一组的数量要加倍
}
if(s>0){//打包完有剩余
cnt++;//*剩余的打包为一组
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt; //*把所有种类的物品都打包后的总组数,对其进行01背包算法
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
将每一种物品都按照上述方法拆分后可以保证有选法能拼凑出打包前所有选法(也就是选几个的问题),由一个一个选优化为一坨一坨地选,将所有种类的物品都做这种处理,最终混在一起,记录一共有多少坨,对这些坨做01背包。
(原来每件物品有 n n n件,分组后就有 l o g 2 n log_2n log2n组,对这些组进行枚举,从而使时间复杂度降为 l o g log log级别。)
例题:分组背包问题
#include <iostream>
using namespace std;
const int N = 1010;
int n,m;
int s[N],v[N][N],w[N][N],f[N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)//*组数
{
cin >> s[i];//每组的数量
for(int j=1;j<=s[i];j++)
{
cin >> v[i][j] >> w[i][j];//*第i组内第j个物品属性
}
}
for(int i=1;i<=n;i++)//*每组
for(int j=m;j>=0;j--)//*枚举容量从大到小,用i-1层防止覆盖数据
for(int k=1;k<=s[i];k++)//*枚举组内第k个物品
{
if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);//*如果当前容量大于当前物品容量才有意义,取最大
}
cout << f[m] << endl;
return 0;
}
线性 D P DP DP
例题:数字三角形
#include <iostream>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N],f[N][N];//存放三角形及每个位置的状态
int main()
{
cin >> n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin >> a[i][j];
for(int i=0;i<=n;i++)//*注意初始化的范围
for(int j=0;j<=i+1;j++)//*边界及边界之外也要初始化,因为会用到
f[i][j]=-INF;
f[1][1]=a[1][1];//顶点的最大值就是自身
for(int i=2;i<=n;i++)//从第二行开始枚举
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);//*两个方向过来的上一个状态的和加现在这点的数值取最大值
int res=-INF;
for(int i=1;i<=n;i++) res=max(f[n][i],res);//*找出最后一行的最大值为答案
cout << res << endl;
return 0;
}
例题:最长上升子序列
复杂度: O ( n 2 ) O(n^2) O(n2)
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N],f[N];//*存数值和状态
int main()
{
cin >> n ;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++)//*找以i为结尾的最长上升子序列
{
f[i]=1;//*初始最长为自身长度为1
for(int j=1;j<i;j++){
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
}
}
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i]);//枚举每个结尾找最长的
cout << res << endl;
return 0;
}
第二次再写这道题目,加深了理解:
枚举 j j j那个循环中,如果 a [ j ] < a [ i ] a[j]<a[i] a[j]<a[i],则比较此时 j j j位置的状态(即以 j j j结尾的最长上升子序列的长度+1(加的这个1是加的 a [ i ] a[i] a[i]位置的一个数),也算是一个集合)与 i i i位置的状态取最大值并更新,以此类推…
就是用前面推完的状态更新后面的状态,所以确定好最初的状态也很关键,比如这里的,将以每一个数结尾的初始长度都初始为1,不管有没有别的数,他自己肯定算一个呀!
再写一遍:((ˉ▽ˉ;)…) 还是要注意初始状态的呀,或者说已知状态,比如f[1]=1,通过第一层for循环中的i=1来实现。
输出路径:没搞明白
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N],f[N],g[N];
int main()
{
cin >> n ;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++)
{
f[i]=1;
g[i]=0;
for(int j=1;j<i;j++){
if(a[j]<a[i])
if(f[i]<f[j]+1){
f[i]=f[j]+1;
g[i]=j;//记录路径
}
}
}
//int res=0;
int k=1;//最优解下标
for(int i=1;i<=n;i++)
if(f[k]<f[i]) k=i;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout << res << endl;
return 0;
}
例题:最长上升子序列 II
复杂度:
O
(
log
n
)
O(\log n)
O(logn)
(其实思想不算是
d
p
dp
dp了,类似于贪心)
#include <iostream>
#include <vector>
using namespace std;
const int N = 100010;
int a[N];
int main()
{
int n; cin >> n;
vector<int>stack;//vector模拟栈,利用lower_bound
for (int i = 0; i < n; i++)
cin >> a[i];
stack.push_back(a[0]);
for (int i = 1; i < n; i++)
{
if (a[i] > stack.back()) stack.push_back(a[i]);//如果要加入的数比栈顶的数要大,那么直接加入
else //如果要加入的数小于等于栈顶的数。那么让它替换掉栈中已有的大于或等于这个数的数
*lower_bound(stack.begin(),stack.end(),a[i]) = a[i];//这里的星号起到解引用的作用,本来函数返回值是迭代器
}
cout << stack.size() << endl;
return 0;
}
例题:最长公共子序列
#include <iostream>
using namespace std;
const int N = 1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main()
{
cin >> n >> m;
// scanf("%s",a+1);
// scanf("%s",b+1);
scanf("%s%s",a+1,b+1); //*需要用到i-1和j-1,从1开始存
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1]);//*有重复,但不影响
if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
cout << f[n][m] << endl;//*最后一个一定是最长的,从前往后推的
return 0;
}
讲的太好了!
/*如果两个都不相等,等价于 f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] f[i][j]=f[i-1][j-1] f[i][j]=f[i−1][j−1],而 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) f[i][j]=max(f[i-1][j],f[i][j-1]) f[i][j]=max(f[i−1][j],f[i][j−1])*/
啥也没说,思路有点混了。。。
例题:最短编辑距离
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N],b[N];
int n,m;
int f[N][N];
int main()
{
cin >> n >> a + 1 >> m >> b + 1;//从下标1开始读入,防止后面i-1,j-1越界
for (int i = 1; i <= n; i++) f[i][0] = i;//A的前i个与B的前0个字母匹配,有几个删几个
for(int i = 1; i <= m; i++) f[0][i] = i;//A的第0个字母与B的前i个字母匹配,B有几个A增加几个
//为什么不需要考虑从第0个开始呢?考虑也行,不过没必要,因为全局数组都已经初始化好了为0
for (int i = 1; i <= n; i++)//必须先枚举A再枚举B
for (int j = 1; j <= m; j++)//因为f[][]是表示从A到B的状态
{
f[i][j]=min(f[i-1][j] + 1,min(f[i][j - 1] + 1, f[i - 1][j - 1] + (a[i] != b[j])));
}
cout << f[n][m] << endl;
return 0;
}
小插曲!
又回来学 D P DP DP了!
放下了再拾起来,又对 D P DP DP有了新的或者说不同角度的认识和理解,突然感觉如果一直陷在一个地方很可能会迷失欸,想起之前一直麻木地学这个,但是同时也缺少了一些其他角度的思考…
再回首突然感觉 D P DP DP没必要看得那么复杂,(虽然难的题是真的复杂…),就是想他会有几种状态,这几种状态会从什么状态转移过来,要取哪种属性,还有一个特别重要的是!!!初始情况,就是说要将一些边界值按照题意及推导合理初始化,然后后面的递推公式都会建立在此初始化的前提之上。
例题:编辑距离
就是上一题的变形,注意读入不从0开始,用 c i n cin cin的话要加括号,( s c a n f scanf scanf的话就不需要),另外也要注意,真假条件做为值的话要给条件加括号。。。
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
const int N = 1010, M = 15;
int n, m;
char str[N][M];
int f[N][M];
int edit_dist(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);//从下标1开始计算长度
for (int i = 0; i <= la; i++) f[i][0] = i;//i从0还是1都无所谓
for (int i = 0; i <= lb; i++) f[0][i] = i;//只不过0其实已经初始化过了,就是f[0][0]==0不用变
for (int i = 1; i <= la; i++)
for (int j = 1; j <= lb; j++)
{
f[i][j] = min(f[i-1][j] + 1,min(f[i][j - 1] + 1,f[i - 1][j - 1] + (a[i]!=b[j])));
}
return f[la][lb];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> (str[i] + 1); //从str[0][1]读入
while (m--)
{
char b[M];
int lim;
cin >> (b + 1) >> lim;
int res = 0;
for (int i = 0; i < n; i++)
{
if(edit_dist(str[i], b) <= lim)//str的第i行,也就是第i个字符串比较
res++;
}
cout << res << endl;
}
return 0;
}
区间 D P DP DP
例题:石子合并
#include <iostream>
using namespace std;
const int N = 310;
int n;
int s[N],f[N][N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> s[i];
for(int i=1;i<=n;i++) s[i]+=s[i-1];//求前缀和
for(int len=2;len<=n;len++)
for(int i=1;i+len-1<=n;i++)
{
int l=i,r=i+len-1; //区间范围
f[l][r]=1e9; //*初始化后续做比较取最小
for(int k=l;k<r;k++)
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
cout << f[1][n] << endl;
return 0;
}
坑🕳: f o r ( i n t l e n = 2 ; l e n ≤ n ; l e n + + ) for(int\ len=2;len\le n;len++) for(int len=2;len≤n;len++) 竟然长度是从 2 2 2枚举到 n ? ? n?? n??
不能理解。。。到 n − 1 n-1 n−1输出一直为 0 0 0…
好像知道了,下一步循环要用到 l e n len len, l e n len len到 n n n后面求右边界才能到达 n n n的位置。
计数类DP
例题:整数划分
(可以用完全背包思想来做,做法如下)
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N]; //f[i][j] 1~i种数和为j的方案数,优化后将第一维去掉,并从小到大枚举,确保用第i层的数据
int main()
{
int n; cin >> n;
f[0] = 1;//选前0个数和为0的方案有1种,其余的方案不可能使和为0,所以其他的初始化都为0
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
hhhhh,代码好短!好久没写过这么短的代码了!
思想大概就是:从1~n
每个数可以选无数次(当然加起来是
n
n
n),相当于完全背包的每种物品可以选无数次,并且也可以将二维状态方程优化为一维。
f[i][j] = f[i-1][j] + f[i-1][j-i] + f[i-1][j-i*2]+ ... + f[i-1][j-i*s]
f[i][j-i] = f[i-1][j-i] + f[i-1][j-2*i] + f[i-1][j-i*3]+ ... + f[i-1][j-i*s]
我们发现可以优化成:
f[i][j] = f[i-1][j] + f[i][j-i]
并且根据经验,可以利用滚动数组,将第一维去掉,状态转移方程变为f[j] = f[j] + f[j-i]
第二种奇妙的状态表示方法:
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N][N]; //f[i][j] 和为i 分为j个数的方案数
int main()
{
int n; cin >> n;
f[0][0] = 1;//和为0分为0个数的方案有1种
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)//i最多分为i个数
f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
int res = 0;
for (int i = 1; i <= n; i++ ) res = (res + f[n][i]) % mod;
cout << res << endl;
return 0;
}
对于两种状态划分:第一种表示我们去掉一个1
,那么此时和减小1
,数的个数减小1
,第二种既然最小值都大于1
,那么我们将这j
个数每个都减1
,那么总体减了j
,但是数的个数还是j
,由此得到状态转移方程~
数位统计DP
例题:计数问题
# include <iostream>
# include <cmath>
using namespace std;
int dgt(int n) // 计算整数n有多少位
{
int res = 0;
while (n) ++ res, n /= 10;
return res;
}
int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次
{
int res = 0, d = dgt(n);
for (int j = 1; j <= d; ++ j) // 从右到左第j位上 数字i出现多少次
{
// l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字
int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
// 计算第j位左边的整数小于l (视频中l = 000 ~ abc - 1)的情况 左边不等于abc的时候 说明都是比abc小的数字
if (i) res += l * p; //如果不是统计数字0 左边直接乘p就行了 n=ab3xxx p=1000
//n=1236055 6000-6999这里1000 第j位上的6出现了p次 但是左边还有16000-16999 26000-26999 36000-36999...1226000-1226999 共左边数字l(即123)个 所以是l*p
else if (!i && l) res += (l - 1) * p; // 统计的数字i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1)
//少了0000-0999的一种情况 从10000-10999 开始 ... 1220000-1220999 13000-13999 共(l-1)次
// 计算第j位左边的整数等于l (视频中l = abc)的情况 只会和*j位后面的数*有关
//下面就是l的左边相等的情况 对第j位上 不会多算6000-6999 ...1226000-1226999里面的任意个集合 123开始的情况
if ( (dj > i) && (i || l) ) res += p;//第j位比现在统计的数字大 就可以直接加上p中情况
// n=1236055 则有1235000-1235999 999+1种情况 即p种
//当统计的数字i==0 且 l==0, 举例 n=123456 l==0 第j位为1 就是p=100000 此时000000-099999是不成立的 因为我要统计第j位为i的时候 有多少个这样的 数 而此时 000000-099999 显然和 100000-199999 第j-1位为2的时候重复了
if ( (dj == i) && (i || l) ) res += r + 1;//这是r有多少个 就是多少个+1
//if(dj==i) n=1236055 1236000-1236055 即55+1种情况
//当统计的数字i==0 且 l==0, 举例 n=123456 l==0且i==0 就是000000 -0123456 而这个时候显然和 第j-1的位的时候重复了100000-109999
//if(dj>i) n=1236000 则有1237000-1237999 所以是0
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b);
for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
/*链接:https://www.acwing.com/solution/content/7128/
来源:AcWing
*/
一位大佬的代码详细注释!
这个真的好复杂!不难但是很烦!
(芜湖~基础课 D P DP DP到此完结咯!)
状态压缩DP
(一般会有比较明显的特点:数据范围不大,20左右,因为 2 20 2^{20} 220范围就 1 e 6 1e6 1e6左右了)
先从简单一点的开始吧QAQ
例题:最短Hamilton距离
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 21, M = 1 << N; //这里N开到22 会超内存
int f[M][N],w[N][N];//不要忘记f[][]的一维表示整个图的各个点是否走过的二进制状态
int n;
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> w[i][j];
//因为要求最小值,先把所有值初始化为正无穷
memset(f, 0x3f, sizeof f);
//设置初始状态
f[1][0] = 0; //初始在0的位置, 1 是二进制表示中第0位为1,表示点0被走过了,从0到0,长度为0
//枚举所有状态
for (int i = 0; i < 1<<n; i++)//0 ~ 1111111111...
for (int j = 0; j < n; j++)
if (i >> j & 1)//如果i路线中包含才可行
for (int k = 0; k < n; k++)//枚举中间点
if ((i - (1 << j)) >> k & 1)//如果i路线去掉j点后包含k点才可行
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1] << endl;//每个点都被走了,到终点n-1的结果
return 0;
}
跟求最短路径的 D i j k s t r a Dijkstra Dijkstra很像(?)
例题:蒙德里安的梦想
f [ i ] [ j ] f[i][j] f[i][j]表示已经将前$ i -1$ 列摆好,且从第 i − 1 i−1 i−1列,伸出到第 i i i 列的状态是 j j j 的所有方案。 i i i代表列号, j j j代表一列的二进制表示的状态( 0 0 0代表这一列的这一行没有方块, 1 1 1代表这个位置有方块)。
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N; //棋盘大小 状态数
LL f[N][M];//状态转移
bool st[M]; //每一列的状态是否符合要求 M!!!!!
vector<int> state[M];
int main()
{
int n, m;
while(cin >> n >> m && n || m)
{
for (int i = 0; i < 1 << n; i++) //枚举每一列的每种状态
{
int cnt = 0; //记录间隔是否是偶数(是否可以放竖的方块)
bool is_valid = true;
for (int j = 0; j < n; j++)//枚举每种状态在每一行(某一列)是否有方块占据了
{
if (i >> j & 1)//如果这一列的这一层有方块(前一列伸过来的
{
if (cnt & 1)//且前面间隔奇数个空位置
{
is_valid = false;
break;//不合法继续枚举下一列
}
//cnt = 0;如果这一层有方块了且前面间隔是偶数个那么计数置为零 不加也行(?)可能只考虑奇偶性吧
}
else cnt++;//是空的计数++
//if (cnt & 1) is_valid = false;
}
if (cnt & 1) is_valid = false; //如果最后还有一些间隔判断这些间隔是否符合要求(也可能这一列的以填了方块结束,那么cnt==0
st[i] = is_valid; //这一列的这种二进制状态合理
}
for (int i = 0; i < 1 << n; i++)
{
state[i].clear();
for (int j = 0; j < 1 << n; j++)//枚举所有二进制状态
if ( (i & j) == 0 && st[i | j]) // ==0 !!!!! 如果在同一行没有重叠且两列合在同一列的状态在上面求到的是合理的
state[i].push_back(j);//把这两列的信息存储在二维数组中
}
memset(f, 0, sizeof f);
f[0][0] = 1;//-1列没有伸到0列 算一种
for (int i = 1; i <= m;i++)//枚举每一列,一直到最后一列的后一列,从第0列开始
for (int j = 0; j < 1 << n; j++) //这一列的这种二进制状态合理
{
for (auto k : state[j])//遍历这一列的合理状态
f[i][j] += f[i - 1][k];
}
cout << f[m][0] << endl;//前m-1列伸到m列的数量为0,即摆满了
}
return 0;
}
树形DP
例题:没有上司的舞会
两种状态:选与不选当前根节点
属性:最大值
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int has_father[N],happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];
int n;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
f[u][1] = happy[u]; //初始先加上根节点的happy值
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);//不选根节点,那他的儿子们可选可不选根节点取最大值
f[u][1] += f[j][0];//选了根节点那他的儿子们就不能选根节点了
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++)//下标必须从1开始,题目中规定好了的
cin >> happy[i];
for (int i = 0; i < n - 1; i++)
{
int a, b;
cin >> a >> b;
has_father[a] = true;
add(b, a); //a 加到 b
}
int root = 1;
while(has_father[root]) root++;//依次查找找到根节点
dfs(root);
cout << max(f[root][0],f[root][1]) << endl;
return 0;
}
记忆化搜索
例题:滑雪
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int h[N][N],f[N][N];
int r,c;
const int dx[]={1,0,-1,0},dy[]={0,-1,0,1};
int dp(int x, int y)
{
int &v = f[x][y];
if (v != -1) return v;//计算过了直接返回答案
v = 1;//没有计算过 初始化为1
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a >=1 && a <= r && b >=1 && b <=c && h[a][b] < h[x][y])
v = max(v, dp(a, b) + 1);
}
return v;
}
int main()
{
cin >> r >> c;
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
cin >> h[i][j];
memset(f, -1, sizeof h);
int res = 0;
for (int i = 1; i <= r; i++)
for (int j = 0; j <= c; j++)
res = max(res,dp(i, j));
cout << res << endl;
}
像 d f s dfs dfs…
(完结!★,°:.☆( ̄▽ ̄)/$:.°★ 。!开心!)