数字三角形模型
- 当前的状态由上一层与当前状态相关的两个或多个状态转移过来(上一层的状态已知)例如:杨辉三角形
- 当路线是双路甚至是多路的情况时,可以用 k=i1+j1,k=i2+j2,中 k 表示状态减少 dp 的纬度,例如:dp[i1][j1][i2][j2]=dp[k][i1][i2]
背包模型
背包问题的初始化
- 对于最值问题
- 对于状态合法
- 如果状态定义为小于等于v 的状态,那0是所有状态的初始化
- 状态转移时,dp[j]=max/min(dp[j],dp[j-v]+w);
- 如果状态定义为恰好为v的状态,那么dp[0]=0,其他状态则是不合法的状态
- 状态转移时,dp[j]=max/min(dp[j],dp[j-v]+w);
- 如果状态定义为大于等于v的状态,那么dp[0]=0,其他状态则是不合法状态
- 状态转移时,dp[j]=max/min(dp[j],dp[max(j-v,0)]+w);
- 如果状态定义为小于等于v 的状态,那0是所有状态的初始化
- 对于状态不合法情况,对于max,不合法的初始化为-0x3f3f3f3f,对于min,不合法状态为0x3f3f3f3f
- 对于状态合法
- 对于方案数问题,只需要初始化dp[0]=1,其他状态保持0即可
- 状态转移 dp[j]+=dp[j-v]
01背包
- 每件物品 体积:v 价值:w 数量:1
f[i][j]=Max(f[i-1][j],f[i-1][j-v]+w)
f[j]=Max(f[j],f[j-v]+w)
#include<iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v>>w;
for(int j=m;j>=v[i];j--)
dp[j]=max(dp[j],dp[j-v]+w);
}
cout<<dp[m];
return 0;
}
- 先考虑是滚动数组的优化,像在表达式中,只有 i 层 和 i-1 两层时,可用滚动数组
- 然后考虑如 j 和 j-w 这样 j-w 严格在 j 的一侧的时候,可以直接优化成一维
- 由于 i 层是由 i - 1 层来转化的,需要考虑的是怎么样防止 i-1 层的 j- w 要不先被更新
- 很明显,当前 i 层的值 j 需要左边的 i - 1 层的 j - w 值来更新的时候,需要从右边开始更新,防止 j - w 值先被更新成 i 层
- 反之,如果 这一层的值需要上一层的右边值来更新的话,那就先从左边开始更新
完全背包
- 每件物品 体积:v 价值:w 数量:无穷
f[i,j]=Max(f[i-1][j], f[i-1][j-v]+w,f[i-1][j-2v]+2w....f[i-1][j-kv]+kw)
f[i,j-v]=Max(f[i-1][j-v], f[i-1][j-2v]+w,f[i-1][j-3v]+2w....f[i-1][j-kv]+(k-1)w)
f[i][j]=Max(f[i-1][j],f[i][j-v]+w)
f[j]=Max(f[j],f[j-v]+w)
- 与 01 背包问题不同,完全背包问题的 i 层状态是通过 i 层来更新的,所以要先更新转移前的状态 也就是从左边开始更新
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v>>w;
for(int j=v[i];j<=m;j++)
dp[j]=max(dp[j],dp[j-]+w);
}
cout<<dp[m];
return 0;
}
多重背包
- 每件物品 体积:v 价值:w 数量:s
f[i,j]=Max(f[i-1][j], f[i-1][j-v]+w,f[i-1][j-2v]+2w....f[i-1][j-sv]+sw)
f[i,j-v]=Max(f[i-1][j-v], f[i-1][j-2v]+w,f[i-1][j-3v]+2w....f[i-1][j-sv]+(s-1)w
+f[i-1][j-(s+1)v]+sw)//多出一块:f[i-1][j-(s+1)v]+sw
//无法通过完全背包问题优化
//二进制优化:
1,2,4,8, ... ,512
//从二进制角度来看:其实每个数都是二进制中的一个位为1的数
//所以加在一起能够表示的是(第n号位*2-1)的数
//此时的总数量是n*logs个
//多重背包二进制优化为01背包问题
//单调队列优化
//dp[i][j] = (dp[i-1][j],···,dp[i-1][r](r为j%v))
//(一共有s个)中最大值dp[i-1][k]+(j-k)/v*w
//枚举r,通过r来得到每一个j值,dp[i][j]由dp[i-1][j···j-s*v](一共s个)中最大值组成
//用单调队列存储从r开始的j值,通过单调队列维持区间j··j-s*v且单调队列对头为区间最大值
//从r开始更新所有以r为模的体积的最大值
二进制优化
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1100;
int v[N],w[N],dp[N];
int main()
{
int n,m;
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)
{
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=k*a;
w[cnt]=k*b;
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
v[cnt]=s*a;
w[cnt]=s*b;
}
}
n=cnt;
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[m];
return 0;
}
单调队列优化
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 20010;
int n, m;
int v[N], w[N], s[N];
int dp[M],g[M];
int q[M];
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)
{
memcpy(g,dp,sizeof g);//g储存上一层的所有值
for (int r = 0; r < v[i]; ++ r)//枚举余数j%v
{
int hh = 0, tt = -1;//单调队列
for (int j = r; j <= m; j += v[i])//枚举所有体积
{
while (hh <= tt && j - q[hh] > s[i] * v[i]) hh ++ ;//维持队列区间j--j-s*v
//维护单调队列
while (hh <= tt && g[q[tt]] + (j - q[tt]) / v[i] * w[i] <= g[j]) -- tt;
q[ ++ tt] = j;
//更新
dp[j] = g[q[hh]] + (j - q[hh]) / v[i] * w[i];
}
}
}
cout << dp[m] << endl;
return 0;
}
分组背包
- 每件物品 体积:v 价值:w 分为 N 组,每组只能选一个物品
f[i][j]=Max(f[i-1][j],f[i-1][j-v[i,k]]+w[i,k])
f[i][j]=Max(f[j],f[j-v[i,k]]+w[i,k])
- 与 01 背包问题类似的遍历方式,多一层组的循环,j 的循环依旧是从左边开始
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int dp[N],w[N][N],v[N][N],s[N];
int main()
{
int n,m;
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];
}
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
for(int k=1;k<=s[i];k++)
{
if(v[i][k]<=j)
dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
}
}
}
cout<<dp[m];
return 0;
}
线性 DP(最长上升子序列模型)
- 当前的状态由前面的状态转移过来(一般遍历前面的所有状态选最优状态)
- 如果要先上升再下降,就先分别求出顺序最长上升和逆向最长上升,再遍历所有状态的两个上升的序列值相加,得到 max 值
- 多组上升子序列
- 对于上升序列,其最大上升序列的每个值都严格大于其他上升序列的值
- 所以只需要依次从大到小遍历所有序列,得到合适位置插入即可
- 对于多组下降子序列同理
最长上升子序列
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
- 当前点的状态由前面所有比它小的点的中最长的值转移过来
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int nums[N];
int dp[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>nums[i];
int ans=1;
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=i-1;j>0;j--)
{
if(nums[j]<nums[i])
{
dp[i]=max(dp[i],dp[j]+1);
ans=max(ans,dp[i]);
}
}
}
cout<<ans;
return 0;
}
//二分优化 :
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 101000;
int q[N];
int a[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
q[0]=-0x3f3f3f3f;
int len=0;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(r>l)
{
int mid=l+r+1>>1;
if(q[mid]<a[i])l=mid;
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i];
}
cout<<len<<endl;
return 0;
}
区间 DP
- 从小到大枚举区间大小 len
- 枚举区间左端点 l ,同时根据区间大小 len 和左端点 l 计算出区间右端点 r=l+len
- 通过状态转移方程求 dp[l] [r] 的值
有 N 堆石子排成一排,其编号为 1,2,3,…,N1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,
合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
- 区间 a 到 b 整体合并,可以划分到更小子区间(a-k,k-b)的合并,k 可以是 a-b 的任何一个点,取最小
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int dp[N][N];
int s[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>s[i];
s[i]+=s[i-1];
}
for(int k=1;k<n;k++)
{
for(int i=1;i+k<=n;i++)
{
int j=i+k;
dp[i][j]=1e9;
for(int t=i;t<j;t++)
{
dp[i][j]=min(dp[i][j],dp[i][t]+dp[t+1][j]+s[j]-s[i-1]);
}
}
}
cout<<dp[1][n];
return 0;
}
数位统计 DP
分析
- 设数N=a(n) a(n-1) a(n-2)...a(1) a(0)
- 当高位取到0 - a-1时,低位可以任意取
- 对其分析有:
- 最终的答案是 所有树中的0 - a-1的方案加上最后一颗子树的a(0)的方案
void init()//初始化所有情况的方案数
int dp(int n)//dp(n)表示0-n所有数中符合要求的数的数量,对于区间数量 dp(r)-dp(l-1)
{
if(!n)return 0;//特判n==0的情况
vector<int>nums;
while(n)nums.push_back(n%10),n/=10;
int res=0;//总的数量;
int last=0;//对当前位的前面所有位的情况记录;
for(int i=nums.size()-1;i>=0;i--)
{
int x=nums[i];
//对所有情况做判断,并统计最终结果
if(!i)//对=最后一颗子树的a(0)的方案判断
}
return res;
}
题目
- 分类讨论
//计数问题
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…
输入包含多组测试数据。
每组测试数据占一行,包含两个整数 a和 b。
当读入一行为 0 0 时,表示输入终止,且该行不作处理。
- 实现一个find(x,k)的函数,表示 1~x 中 k 出现的次数,则 a~b 中 i 出现的次数就是 find(b,i)-find(a-1,i)
- 对于 0000000 ~ abc d efg,假设求 4 位出现 k 则数为 xxxkyyy:
- 当000<=xxx<=abc-1,则这个数严格小于 abcdefg,这样的情况个数有:abc*(10^3)
- 当 d==0,这时xxx!=000,此时000<xxx<=abc-1,情况个数:(abc-1)*(10^3)
- 当xxx==abc,对 d 进行分类:
- 当d<k,此时abckyyy>abcdefg 无解
- 当d==k,则只有000<=yyy<=efg 有解 ,情况个数:efg+1
- 当d>k, 则abcdefg>abckyyy,情况个数:(10^3)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int to_int(vector<int>num,int l,int r)
{
int ans=0;
for(int i=l;i>=r;i--)
{
ans=ans*10+num[i];
}
return ans;
}
int to_10(int k)
{
int ans=1;
while(k--)ans*=10;
return ans;
}
int find(int x,int k)
{
if(!x)return 0;
vector<int>num;
while(x)num.push_back(x%10),x/=10;
int n=num.size();
int ans=0;
for(int i=n-1-!k;i>=0;i--)
{
if(i<n-1)
{
ans+=to_int(num,n-1,i+1)*to_10(i);
if(!k)ans-=to_10(i);
}
if(num[i]==k)ans+=to_int(num,i-1,0)+1;
else if(num[i]>k)ans+=to_10(i);
}
return ans;
}
int main()
{
int a,b;
while(cin>>a>>b,a||b)
{
if(a>b)swap(a,b);
for(int i=0;i<10;i++)
cout<<find(b,i)-find(a-1,i)<<" ";
cout<<endl;
}
return 0;
}
状态压缩 DP
- 时间复杂度计算:状态数量*状态计算的计算量
棋盘式
蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。
- 状态dp[i,j]表示:第 i 列中,存在以 从i-1列伸出的所有组成的j的最大方案数
- (例如:总共 5 行,第 1,3,5 行有从i-1列的横向 1*2 方块占有第i列,所以dp[i,j]就是dp[i,10101],用j的·二进制表示第i列的状态)
- 要使得dp[i-1,k]转移到dp[i,j]需要满足
- j&k==0
- 表示第 i 行和第 i-1 行的横向方块没有冲撞
- j|k不存在连续奇数个 0
- 表示第 i 行的 j 是由第 i - 1 行放的横向和纵向转移来
- 如果有三个连续的 0,那很明显最后一行或者第一行是可以放一个横向剩下放个竖向,或者直接放三个横向的,那就说明第 i 行的 j 不是由第 i-1 行的 k 转移
- 如果就一个 0 一样可以是放一个横向
- 所以只有偶数时,说明那放的是竖向的,此时才说明转移是正确的 每组测试用例占一行,包含两个整数 N 和 M。 当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
- 表示第 i 行的 j 是由第 i - 1 行放的横向和纵向转移来
- j&k==0
#include<iostream>
#include <cstring>
using namespace std;
const int N = 12,M=1<<N;
long long dp[N][M];
bool st[M];
int main()
{
int n,m;
while(cin>>n>>m,n||m)
{
memset(dp, 0, sizeof dp);
for(int j=0;j<1<<n;j++)//列举所有二进制状态
{
int cnt=0;//cnt 记录连续0的个数
st[j]=true;
for(int i=0;i<n;i++)//j表示的是所有行的转态,所以有n位
{
if(j>>i&1)//当前位是1
{
if(cnt&1)st[j]=false;//前面有奇数个0
cnt = 0;//更新cnt
}else cnt++;//当前位为0,cnt++
}
if(cnt&1)st[j]=false;//判断最后一次cnt
}
dp[0][0]=1;//第0列没有从-1列的横向
for(int i=1;i<=m;i++)//枚举到第m列
for(int j=0;j<1<<n;j++)
for(int k=0;k<1<<n;k++)
if((j & k)==0&&st[j|k]) dp[i][j]+=dp[i-1][k];
cout<<dp[m][0]<<endl;//不存在第m列,所以dp[m][0]其实就表示前面的所有方案和
}
return 0;
}
集合
最短Hamilton路径
给定一张 n个点的带权无向图,点从 0∼n−1 标号,求起点 0到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1不重不漏地经过每个点恰好一次。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20,M=1<<N;
int w[N][N];
int dp[M][N];
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(dp, 0x3f, sizeof dp);
dp[1][0]=0;
for(int i=0;i<1<<n;i++)
for(int j=0;j<n;j++)
if(i>>j&1)
for(int k=0;k<n;k++)
if((i - (1<<j) ) >>k & 1)dp[i][j]=min(dp[i][j],dp[i - (1<<j)][k]+w[k][j]);
cout<<dp[(1<<n)-1][n-1]<<endl;
return 0;
}
树形 DP
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入格式
第一行一个整数 N。
接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。
接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。
- 状态表示
- dp[u][0]
- 表示以 u 为根节点且不包含 u 的最大值
- dp[u][0]+=max(dp[w][0],dp[w][1])w 为所有 u 的子节点
- dp[u][1]
- 表示以 u 为根节点且=包含 u 的最大值
- dp[u][1]+=dp[w][0]w 为所有 u 的子节点
- dp[u][0]
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int dp[N][2],happy[N],h[N], e[N], ne[N], idx;
bool fa[N];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
dp[u][1]=happy[u];
for(int i=h[u];i!=-1;i=ne[i])
{
int w=e[i];
dfs(w);
dp[u][1]+=dp[w][0];
dp[u][0]+=max(dp[w][0],dp[w][1]);
}
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>happy[i];
memset(h, -1, sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b;
cin>>a>>b;
add(b, a);
fa[a]=true;
}
int k=1;
while(fa[k])k++;//找到根节点
dfs(k);
cout<<max(dp[k][0],dp[k][1])<<endl;
}
记忆化搜索
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。 矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 jj列区域的高度。 一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离
- dp[i][j] 表示从( i ,j )出发的最长路径
- dp[i][j] 由 4 个方向中比( i ,j )点小的状态 +1 转移
- 在每次搜索时,会同时搜索到 其余部分点的状态,对这些点搜索过的直接返回值
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int dp[N][N];
int nums[N][N];
int dx[4]={0,-1,1,0};
int dy[4]={-1,0,0,1};
int n,m;
int dfs(int x,int y)
{
int &v=dp[x][y];
if(v!=-1)return v;
v=1;
for(int i=0;i<4;i++)
{
int nx=dx[i]+x;
int ny=dy[i]+y;
if(nx>=0&&ny>=0&&nx<n&&ny<m&&nums[nx][ny]<nums[x][y])
v=max(v,dfs(nx,ny)+1);
}
return v;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
cin>>nums[i][j];
}
}
memset(dp, -1, sizeof dp);
int res=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
res=max(res,dfs(i,j));
}
}
cout<<res<<endl;
}