DP专题
树形dp
P2014 选课
题目链接:https://www.luogu.org/problem/P2014
树形dp一般分为两种 一种是选节点类 一种是树形背包类 这一题是典型的树形背包类
我们可以发现这个是森林而不是树状结构 所以我们可以虚拟一个节点0 这个节点成为新的根节点
树形dp就是从底部到顶部 每棵子树选最优解 然后向根节点转移 这一过程可以用dfs实现
接下来就是推转移方程
dp[i][j]定义以i为子树选j个节点的方案数 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]) v为i的子树中的节点 但是这里要注意一点 这里有一个依赖关系 如果我们要选择i的子树 那么必定要选择i 所以转移方程实际上 dp[i][j]=max(dp[i][j],dp[i][j-k-1]+dp[v][k]) 所以我们只要控制j-k>=1即可 然后对于当前以u为根节点的子树来说 对于方案数 相当于一维背包 所以要思考是顺序还是倒序 我们可以发现 如果是j=1~m 那么dp[u][j]会被前面已经算出的dp[u][j-k]影响(相当于选了多次)所以要倒序枚举
而且对于建图的过程 要考虑清楚是单向边还是双向边 如果指定了根节点就是单向边 如果未指定根节点(只说明了两个节点连接)那么就是双向边
且对于双向边 必须用一个变量father控制 因为双向边的dfs遍历会导致一直在一条边的两个点遍历 比如这一题
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 300+10; const int mod = 1e9+7; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int tot,head[maxn]; int w[maxn],dp[maxn][maxn]; int n,m; struct Edge { int dest,next; } edge[maxn*2]; void add(int u,int v) { edge[++tot].next=head[u]; edge[tot].dest=v; head[u]=tot; } void dfs(int u) { dp[u][1]=w[u]; for(int i=head[u]; i; i=edge[i].next) { int v=edge[i].dest; dfs(v); for(int j=m; j>=1; j--) //根节点选了j个 { for(int k=j; k>=0; k--) //子树选了k个 dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } } } int main() { scanf("%d %d",&n,&m); memset(head,0,sizeof(head)); for(int i=1; i<=n; i++) { int a,b; scanf("%d %d",&a,&w[i]); add(a,i); } m++; dfs(0); printf("%d\n",dp[0][m]); }
P2015 二叉苹果树
题目链接:https://www.luogu.org/problem/P2015
对比上一题 这题不一样的就是双向边 所以需要一个fa变量使dfs除了回溯不会向上搜索 然后还需要注意根节点是一定需要选择的 所以选择的节点数要+1
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 300+10; const int mod = 1e9+7; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int tot,head[maxn]; int w[maxn],dp[maxn][maxn]; int n,m; struct Edge { int dest,next,weight; } edge[maxn*2]; void add(int u,int v,int w) { edge[++tot].next=head[u]; edge[tot].dest=v; edge[tot].weight=w; head[u]=tot; } void dfs(int u,int fa) { for(int i=head[u];i;i=edge[i].next) { int v=edge[i].dest; if(v==fa) continue; int d=edge[i].weight; dp[v][1]=d; dfs(v,u); for(int j=m;j>=1;j--) { for(int k=j-1;k>=0;k--) { dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } } } } int main() { scanf("%d %d",&n,&m); m++; for(int i=1;i<n;i++) { int u,v,w; scanf("%d %d %d",&u,&v,&w); add(u,v,w); add(v,u,w); } dfs(1,1); printf("%d\n",dp[1][m]); }
状压dp
P1879 [USACO06NOV]玉米田Corn Fields
题目链接:https://www.luogu.org/problem/P1879
一道最基础的状压dp 但是我是最近才搞懂的
通过十进制转二进制将每一行的地图压缩成一个十进制位数 其中1代表土地肥沃 0代表土地贫瘠
枚举状态 其中状态中的1代表种草 0代表不种草
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 #define mem(a,b) memset(a,b,sizeof(a)) //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 15; const int mod = 1e8; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int mp[maxn][maxn],dp[maxn][1<<maxn]; int f[maxn]; int cnt[1<<maxn]; int tot; bool check(int x) { if(x&(x<<1)) return 0; if(x&(x>>1)) return 0; return 1; } int main() { int n,m; scanf("%d %d",&n,&m); for(int i=1; i<=n; i++) { for(int j=1; j<=m; j++) scanf("%d",&mp[i][j]); } for(int i=0; i<(1<<m); i++) { if(check(i)) cnt[++tot]=i; } for(int i=1; i<=n; i++) { for(int j=1; j<=m; j++) f[i]=(f[i]<<1)+mp[i][j]; } for(int i=1; i<=tot; i++) { if((f[1]&cnt[i])==cnt[i]) dp[1][i]=1; } for(int i=2; i<=n; i++) { for(int j=1; j<=tot; j++) //枚举这一行的状态 { if((cnt[j]&f[i])==cnt[j]) { for(int k=1; k<=tot; k++) //枚举上一行的状态 { if(!(cnt[j]&cnt[k])) { dp[i][j]=(dp[i][j]+dp[i-1][k])%mod; } } } } } int ans=0; for(int i=1;i<=tot;i++) ans+=dp[n][i],ans%=mod; printf("%d\n",ans); }
P1896 [SCOI2005]互不侵犯
题目链接:https://www.luogu.org/problem/P1896
这个比上一题只是多枚举了一个需要放的国王数 要多开一组记录放的国王数
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 #define mem(a,b) memset(a,b,sizeof(a)) //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 10; const int mod = 1e8; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; ll dp[maxn][1<<maxn][81]; int num[1<<maxn]; int cnt[1<<maxn]; bool check(int x) { if(x&(x<<1)) return 0; if(x&(x>>1)) return 0; return 1; } bool check(int x,int y) { if(x&y) return 0; if(x&(y>>1)) return 0; if(x&(y<<1)) return 0; return 1; } int main() { int n,k,tot=0; scanf("%d %d",&n,&k); for(int i=0; i<(1<<n); i++) { if(check(i)) cnt[++tot]=i; } //处理每一个状态有多少个国王 for(int i=1; i<=tot; i++) { for(int j=0; j<n; j++) { if((cnt[i]>>j)&1) num[i]++; } } //对于第一行 for(int i=1; i<=tot; i++) { if(num[i]<=k) dp[1][i][num[i]]=1; } for(int i=2; i<=n; i++) { for(int j=1; j<=tot; j++) //枚举当前行状态 { for(int z=1; z<=tot; z++) //枚举上一行状态 { if(check(cnt[j],cnt[z])) { for(int nn=num[j]; nn<=k; nn++) //枚举king的个数 { dp[i][j][nn]+=dp[i-1][z][nn-num[j]]; } } } } } ll sum=0; for(int i=1; i<=tot; i++) { sum+=dp[n][i][k]; //printf("%d\n",sum); } printf("%lld\n",sum); }
Maximum Sum
题目链接:https://codeforces.com/gym/101853/problem/E
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 #define mem(a,b) memset(a,b,sizeof(a)) //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 18; const int mod = 1e9+7; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int mp[maxn][maxn],dp[maxn][1<<maxn],cnt[1<<maxn]; bool check(int x) { if(x&(x<<1)) return 0; if(x&(x>>1)) return 0; return 1; } bool check(int x,int y) { if(x&y) return 0; if(x&(y<<1)) return 0; if(x&(y>>1)) return 0; return 1; } int main() { int t; scanf("%d",&t); while(t--) { int n; scanf("%d",&n); memset(dp,0,sizeof(dp)); memset(mp,0,sizeof(mp)); memset(cnt,0,sizeof(cnt)); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { scanf("%d",&mp[i][j]); } } int top=0; for(int i=0;i<(1<<n);i++) { if(!check(i)) continue; cnt[++top]=i; } //第一行的处理 for(int i=1;i<=top;i++) { for(int j=0;j<n;j++) { if((cnt[i]>>j)&1) { dp[1][i]+=mp[1][n-j]; //printf("wa=%d\n",dp[1][i]); } } } for(int i=2;i<=n;i++) { for(int j=1;j<=top;j++) //枚举这一行的状态 { for(int k=1;k<=top;k++) //枚举上一行的状态 { if(!check(cnt[j],cnt[k])) continue; int sum=0; for(int p=0;p<n;p++) { if((cnt[j]>>p)&1) sum+=mp[i][n-p]; } dp[i][j]=max(dp[i][j],dp[i-1][k]+sum); //printf("wa=%d\n",dp[i][j]); } } } int ans=0; for(int i=1;i<=top;i++) ans=max(ans,dp[n][i]); printf("%d\n",ans); } }
数位dp
P2657 [SCOI2009]windy数
题目链接:https://www.luogu.org/problem/P2657
首先看a,b的数据范围是2e9 所以肯定不能从l-r遍历 考虑数位拆分
我们可以先把范围内所有可能的windy数预处理 然后后续直接加上预处理的答案即可
然后就是处理A,B数
定义dp[i][j]为第i位 最高位为j的方案数
a.A,B的长度等于1 ans=i+1 直接返回
b.A,B的长度大于1 ans=10 ( 0~9 )
然后分三种情况处理
1.从2~len-1位 此时所有的 j 从1~9都满足 这里一开始我没明白为什么不是从0开始 因为这里计算的就是满足的个数 既然满足 首先就必须是个数 而超过两位数的不会以0为最高位
2.len位 且j<a[len] 此时从1~a[len]-1累加方案数即可
3.len位 且j=a[len] 此时我们就从len-1位往后递推 每一次固定一位
举个例子 1569 我们先固定1 然后len-1位从0枚举到4 即10xx 11xx 12xx 13xx 14xx 然后固定5 len-2位从0枚举到5 即 150xx 151xx 152xx 153xx 154xx 155xx
但是这里有个坑点 即假如a[i+1]-a[i]小于2 就可以直接返回了 拿1569来说 枚举到len-2的时候 发现6-5<2 所以156x 后面不可能有满足的了
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 15; const int mod = 1e9+7; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int dp[maxn][maxn]; int a[maxn]; void init() { for(int i=0;i<=9;i++) dp[1][i]=1; for(int i=2;i<=10;i++) //枚举有多少位 { for(int j=0;j<=9;j++) //枚举当前位 { for(int k=0;k<=9;k++) //枚举上一位 { if(abs(j-k)>=2) dp[i][j]+=dp[i-1][k]; } } } } int work(int x) { int len=0; int ans=0; memset(a,0,sizeof(a)); while(x) { a[++len]=x%10; x/=10; } //printf("len=%d\n",len); if(len<=1) return x+1; for(int i=0;i<=9;i++) ans+=1; //前len-1位 for(int i=2;i<=len-1;i++) { for(int j=1;j<=9;j++) { ans+=dp[i][j]; } } //第len位 且j<a[len] for(int i=1;i<=a[len]-1;i++) { ans+=dp[len][i]; //printf("%d\n",ans); } //第len位 且j=a[len] for(int i=len-1;i>=1;i--) { for(int j=0;j<=a[i]-1;j++) { if(abs(j-a[i+1])>=2) ans+=dp[i][j]; } if(abs(a[i+1]-a[i])<2) break; } return ans; } int main() { ll a,b; scanf("%lld %lld",&a,&b); init(); printf("%d\n",work(b+1)-work(a)); }
思维dp
乌龟棋
题目链接:https://ac.nowcoder.com/acm/problem/16590
根据题意 卡片最多只有4种 分别是走1 走2 走3 走4 那么可以定义dp[i][j][k][l]为走1 走2 走3 走4的卡片数 然后暴力把所有情况枚举出来
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 360; const int maxm = 105; const int mod = 1000007; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int a[maxn],dp[45][45][45][45],b[5]; int main() { int n,m; scanf("%d%d",&n,&m); for(int i=1; i<=n; i++) scanf("%d",&a[i]); for(int i=1; i<=m; i++) { int p; scanf("%d",&p); b[p]++; } dp[0][0][0][0]=a[1]; for(int i=0; i<=b[1]; i++) for(int j=0; j<=b[2]; j++) for(int k=0; k<=b[3]; k++) for(int l=0; l<=b[4]; l++) { int num=a[1+i+2*j+3*k+4*l]; if(i) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i-1][j][k][l]+num); if(j) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j-1][k][l]+num); if(k) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]+num); if(l) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k][l-1]+num); } printf("%d\n",dp[b[1]][b[2]][b[3]][b[4]]); }
摆花
题目链接:https://ac.nowcoder.com/acm/problem/16576
首先我们可以确定dp[i][j]:前i种花 摆j个位置的方案数 但是每一种花有a[i]个可以放 所以我们可以用一个循环枚举每次放i的个数 从0~a[i] 然后i-1就是从j-0~j-a[i] 最后注意初始化
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 105; const int maxm = 105; const int mod = 1000007; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int a[maxn],dp[maxn][maxn]; int main() { int n,m; scanf("%d %d",&n,&m); for(int i=1; i<=n; i++) scanf("%d",&a[i]); for(int i=0; i<=a[1]; i++) dp[1][i]=1; for(int i=2; i<=n; i++) { for(int j=0; j<=m; j++) { for(int k=0; k<=a[i]; k++) { if(k<=j) dp[i][j]=(dp[i][j]+dp[i-1][j-k])%mod; } } } printf("%d\n",dp[n][m]%mod); }
花匠
题目链接:https://ac.nowcoder.com/acm/problem/16535
题目要求的是两种 第一种是先升后降 第二种是先降后升 所以我们可以定义dp[i][j]:当j==0时 表示当前递增 j==1时 表示当前递减 然后就可以得出状态转移方程:
if(a[i]>a[i-1]) dp[i][0]=dp[i-1][1]+1;
else dp[i][0]=dp[i-1][0];
if(a[i]<a[i-1]) dp[i][1]=dp[i-1][0]+1;
else dp[i][1]=dp[i-1][1];
/**Today you do things people will not do, tomorrow you will do things people can not do.**/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 1e5+10; const int maxm = 105; const int mod = 1e9+7; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; int a[maxn],dp[maxn][2]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); dp[1][0]=dp[1][1]=1; for(int i=2;i<=n;i++) { if(a[i]>a[i-1]) dp[i][0]=dp[i-1][1]+1; else dp[i][0]=dp[i-1][0]; if(a[i]<a[i-1]) dp[i][1]=dp[i-1][0]+1; else dp[i][1]=dp[i-1][1]; } printf("%d\n",max(dp[n][0],dp[n][1])); }
被3整除的子序列
题目链接:https://ac.nowcoder.com/acm/problem/21302
dp[i][j]表示前i个数和为j的总方案数 处理总方案数 然后j%3取和就是答案了
然后方程就出来了
但是这里我一开始对于j>=temp的情况没搞太清楚,我一开始推出的方程是dp[i][j]=(dp[i-1][j-temp]+dp[i-1][temp]+dp[i-1][j]+dp[i][j])%mod; 但是其实我们可以发现dp[i-1][temp]已经包括在dp[i-1][j]中了
/*Today you do things people will not do, tomorrow you will do things people can not do.*/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 55; const int INF = 0x3f3f3f3f; const int mod = 1e9+7; const double PI = acos(-1.0); const double eps = 1e-6; ll dp[maxn][455]; char s[maxn]; int n[maxn]; ll ans=0; int sum[maxn]; int main() { scanf("%s",s); int len=strlen(s); for(int i=1;i<=len;i++) { n[i]=s[i-1]-'0'; dp[i][n[i]]=1; sum[i]=n[i]+sum[i-1]; } for(int i=1;i<=len;i++) { int temp=n[i]; for(int j=0;j<=sum[i];j++) { if(j<temp) dp[i][j]=(dp[i-1][j]+dp[i][j])%mod; else dp[i][j]=(dp[i-1][j-temp]+dp[i-1][j]+dp[i][j])%mod; } } for(int i=0;i<=sum[len];i++) { if(i%3==0) ans=(ans+dp[len][i])%mod; } printf("%lld\n",ans); }
删除子串
题目链接:https://ac.nowcoder.com/acm/problem/15362
这一题关键就在对上个串的尾部元素状态的记录,我们知道假设已经取了i个串,那么我们的决策有三种,第一种取了,然后变化加1长度加1,第二种不取,保持原来的状态。但是这两种都是要建立在我们要加入的字符和我们已经构造的字符的尾部元素不同时,所以我认为这题的难点就是记录尾部元素的状态。第三种尾部元素和加入字符相同,直接插入长度加1即可。
接下来就是维护一个数组,这个数组记录了所有的dp[i][j]尾部元素的状态,当我们删除了加入的字符,状态不变,当我们加入了字符,那么状态转移成加入的字符。
/*Today you do things people will not do, tomorrow you will do things people can not do.*/ #include<bits/stdc++.h> #define ll long long #define lson l,m,cnt<<1 #define rson m+1,r,cnt<<1|1 //priority_queue <int,vector<int>,greater<int> > Q;//优先队列递增 //priority_queue<int>Q;//递减 using namespace std; const int maxn = 1e5+10; const int INF = 0x3f3f3f3f; const double PI = acos(-1.0); const double eps = 1e-6; char s1[maxn]; int dp[maxn][15]; int n,m; int cnt[maxn],pre[maxn][15]; int main() { int flag=0,c1=0,c2=0; scanf("%d %d",&n,&m); getchar(); for(int i=1; i<=n; i++) scanf("%c",&s1[i]); int cut=0; while(s1[cut+1]=='b') cut++; cut=cut+1; for(int i=cut; i<=n; i++) { if(s1[i]=='a') flag=1,c1++; dp[i][0]=c1; } if(m==0) { printf("%d\n",cnt[n]); return 0; } if(flag==0) puts("0"); else { for(int i=1; i<=m; i++) dp[cut][i]=1; pre[cut][0]='a'; for(int i=cut+1; i<=n; i++) { for(int j=1; j<=m; j++) { if(s1[i]!=pre[i-1][j]) { dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]+1); if(dp[i][j]==dp[i-1][j]) pre[i][j]=pre[i-1][j]; if(dp[i][j]==dp[i-1][j-1]+1) pre[i][j]=s1[i]; } else dp[i][j]=dp[i-1][j]+1,pre[i][j]=s1[i]; } } printf("%d\n",dp[n][m]); } }