『有关动态规划技巧与套路的若干讨论』
最近组织了一场\(dp\)专项测试,结果我一道题都没做出来,考完后才发现全部都是具有一定套路的\(dp\)题。所以决定开一个博客,专门来记录\(dp\)当中的一些经典应用模型,这篇博客可能会长期更新。
线性DP
线性\(dp\)是动态规划当中最基础的一种,其技巧更是数不胜数,特别适合出\(NOIP\)普及组\(T3\),\(T4\)题,故我们特以此类\(dp\)进行一些讨论。
双向DP(两遍DP)
Tower
Description
信大家都写过数字三角形问题,题目很简单求最大化一个三角形数塔从上往下走的路径和。走的规则是:(i,j)号点只能走向(i+1,j)或者(i+1,j+1)。如下图是一个数塔,映射到该数塔上行走的规则为:从左上角的点开始,向下走或向右下走直到最底层结束。
1 3 8 2 5 0 1 4 3 8 1 4 2 5 0
路径最大和是1+8+5+4+4 = 22,1+8+5+3+5 = 22或者1+8+0+8+5 = 22。
小S觉得这个问题so easy。于是他提高了点难度,他每次ban掉一个点(即规定哪个点不能经过),然后询问你不走该点的最大路径和。
当然他上一个询问被ban掉的点过一个询问会恢复(即每次他在原图的基础上ban掉一个点,而不是永久化的修改)。
Input Format
第一行包括两个正整数,N,M,分别表示数塔的高和询问次数。
以下N行,第i行包括用空格隔开的i - 1个数,描述一个高为N的数塔。
而后M行,每行包括两个数X,Y,表示第X行第Y列的数塔上的点被小S ban掉,无法通行。
Output Format
M行每行包括一个非负整数,表示在原图的基础上ban掉一个点后的最大路径和,如果被ban掉后不存在任意一条路径,则输出-1。
Sample Input
5 3 1 3 8 2 5 0 1 4 3 8 1 4 2 5 0 2 2 5 4 1 1
Sample Output
17 22 -1
Limitation
\(n\leq1000,m\leq5*10^5\)
Solution
这是经典的双向\(dp\),其思想在于对于一个特殊限定,我们可以在无限定的条件下先做两遍\(dp\),分别从起始状态和目标状态开始,然后在对特殊限定进行特殊处理,可以利用两遍\(dp\)得到的值进行快速的求解若干问题。
那么在这道题中,我们分别需要做两遍\(dp\),\(up_{i,j}\)代表从第一行到\((i,j)\)位置的最大数值和,\(down_{i,j}\)代表从最后一行到\((i,j)\)位置的最大数值和。这两个\(dp\)都是非常容易解决的,其方程如下:\[up_{i,j}=max(up_{i-1,j},up_{i-1,j-1})+num_{i,j},down_{i,j}=max(down_{i+1,j},down_{i+1,j+1})+num_{i,j}\]
然后我们预处理出每一行中使得\(up_{i,j}+down_{i,j}-num_{i,j}\)取得最大值以及次大值的位置,那么对于一个损坏的位置\((x,y)\),若这个位置恰好在该行的最大值位置,显然全局的最大值就是该行的次大值,反之,全局最大值就是该行的最大值,那么我们就可以做到\(O(1)\)回答每一个询问了。
\(Code:\)
#include <bits/stdc++.h> using namespace std; inline void read(long long &k) { long long x=0,w=0;char ch; while (!isdigit(ch)) w |= ch=='-' , ch = getchar(); while (isdigit(ch)) x = (x<<3) + (x<<1) + (ch^48) , ch=getchar(); k = ( w ? -x : x ); } const int N=1020; long long n,m,num ,u ,d ; pair < long long , long long > pos ; inline void input(void) { read(n),read(m); for (int i=1;i<=n;i++) for (int j=1;j<=i;j++) read(num[i][j]); } inline long long val(long long x,long long y) { if ( !x || !y )return -1; return u[x][y] + d[x][y] - num[x][y]; } inline void dp(void) { for (int i=1;i<=n;i++) for (int j=1;j<=i;j++) u[i][j] = max(u[i-1][j],u[i-1][j-1]) + num[i][j]; for (int i=n;i>=1;i--) for (int j=1;j<=i;j++) d[i][j] = max(d[i+1][j],d[i+1][j+1]) + num[i][j]; for (int i=1;i<=n;i++) { long long Max=0,sMax=0; for (int j=1;j<=i;j++) { if ( val(i,j) > Max ) { sMax = Max;Max = val(i,j); pos[i] = make_pair( j , pos[i].first ); } else if ( val(i,j) > sMax ) { sMax = val(i,j); pos[i].second = j; } } } } inline void solve(void) { for (int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); if ( pos[x].first==y ) printf("%lld\n",val(x,pos[x].second)); else printf("%lld\n",val(x,pos[x].first)); } } int main(void) { freopen("tower.in","r",stdin); freopen("tower.out","w",stdout); input(); dp(); solve(); return 0; }
DP换置
Market
Description
在比特镇一共有n 家商店,编号依次为1 到n。每家商店只会卖一种物品,其中第i 家商店的物品单价为ci,价值为vi,且该商店开张的时间为ti。
Byteasar 计划进行m 次购物,其中第i 次购物的时间为Ti,预算为Mi。每次购物的时候,Byteasar会在每家商店购买最多一件物品,当然他也可以选择什么都不买。如果购物的时间早于商店开张的时间,那么显然他无法在这家商店进行购物。
现在Byteasar 想知道,对于每个计划,他最多能购入总价值多少的物品。请写一个程序,帮助Byteasar 合理安排购物计划。
注意:每次所花金额不得超过预算,预算也不一定要花完,同时预算不能留给其它计划使用
Input Format
第一行包含两个正整数n;m,表示商店的总数和计划购物的次数。
接下来n 行,每行三个正整数ci; vi; ti,分别表示每家商店的单价、价值以及开张时间。
接下来m 行,每行两个正整数Ti;Mi,分别表示每个购物计划的时间和预算。
Output Format
输出m 行,每行一个整数,对于每个计划输出最大可能的价值和。
Sample Input
5 2 5 5 4 1 3 1 3 4 3 6 2 2 4 3 2 3 8 5 9
Sample Output
10 12
Limitation
\(n\leq300,m\leq10^5,c_i,M_i\leq10^9,v_i\leq300,T_i,t_i\leq300\)
Solution
这是经典\(dp\)模型\(0/1\)背包的\(dp\)换置。
直观地考虑,这就是一道简单的\(0/1\)背包,但是这个背包的体积很大,不适合直接做。那么我们注意到数据中一个很好的限制:\(v_i\leq300\),也就是说,我们可以考虑一种与价值有关的\(dp\)方式,这就用到\(dp\)换置了。
\(f_{i,j}\)代表前\(i\)家商店得到价值总和为\(j\)时的最小花费,这和普通背包问题的状态刚好相反,但是,其状态转移方程几乎是一样的:\(f_{i,j}=min\{f_{i-1,j-v_i}+c_i,f_{i-1,j}\}\),这样的\(dp\)的时间复杂度就只和\(n,v\)有关了。
那我们如何得到答案呢?首先,我们需要对得到的\(f\)数组进行一些处理,显然有一些不能准确表示为若干个价值之和的位置的\(f\)值是正无穷,那么我们需要用得到更多价值的花费来填充该位置。完成这个操作后,我们就会发现\(f\)是具有单调性的,那么我们就可以二分了,二分找到第一个花费大于预算的下标,其上一个位置就是不超过预算的最大价值。
在这道题当中,对于\(t\)有关时间的限制,我们将商店排成时间升序的,然后通过二分在找到时间上的最早开门时间,就可以通过第二次二分找到\(f\)数组中的答案了。
\(Code:\)
#include <bits/stdc++.h> using namespace std; const int N=301,M=100020,INF=0x3f3f3f3f3f; int n,m,t ; long long f [N*N]; struct market { int c,v,t; }a ; inline bool compare(market p1,market p2) { return p1.t < p2.t; } inline void input(void) { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) { scanf("%d%d%d",&a[i].c,&a[i].v,&a[i].t); t[i] = a[i].t; } } inline void dp(void) { sort(a+1,a+n+1,compare);sort(t+1,t+n+1); for (register int i=1;i<=300*n;++i)f[0][i]=INF*1LL; for (register int i=1;i<=n;++i) for (register int j=0;j<=300*n;++j) if (j>=a[i].v) f[i][j] = min(f[i-1][j],f[i-1][j-a[i].v]+a[i].c); else f[i][j] = f[i-1][j]; for (register int i=1;i<=n;++i) for (register int j=300*n-1;j>=0;--j) f[i][j] = min(f[i][j],f[i][j+1]); } inline void solve(void) { for (register int i=1;i<=m;++i) { int T,M,ans=0; scanf("%d%d",&T,&M); int pos = upper_bound(t+1,t+n+1,T)-t-1; ans = upper_bound(f[pos],f[pos]+300*n+1,M*1LL)-f[pos]-1; printf("%d\n",ans); } } int main(void) { input(); dp(); solve(); return 0; }
费用提前计算
Value
Description
给定\(n\)个物品,每个物品价值为\(v_i\)代价为\(w_i\)。
可以以任意顺序选择任意数量的物品,但在选择\(i\)号物品以后,剩下物品的价值就会减少\(w_i\),要求最大化选择商品的价值之和。
Input Format
第一行包括一个整数\(n\),剩下\(n\)行每行包括两个整数\(v_i,w_i\)。
Output Format
一行包括共一个整数,代表价值之和的最大值。
Sample Input
5 8 2 10 7 5 1 11 8 13 3
Sample Output
27
Limitation
\(n\leq5000,v_i,w_i\leq10^5\)
Solution
这道题的每一个物品选择都会对剩下的物品选择造成影响,如果直接\(dp\)的话将会出现后效性,导致\(dp\)错误,那么我们就需要对原来的数据做一些处理,然后利用费用提前计算的技巧,进行动态规划。
首先,对于购买物品的最优组合\(S=\{(v_{p_1},w_{p_1}),(v_{p_2},w_{p_2}),...,(v_{p_k},w_{p_k})\}\),显然按照\(w\)升序购买时收益最大。但是我们需要考虑每一个物品对之后物品的影响,所以我们要将所有物品按照\(w\)降序排序,然后设置倒序的状态:\(f_{i,j}\)代表到物品\(i\)为止,已经选了后\(j\)个物品的最大价值和。这样,对于每一个物品,我们只考虑是否选它作为倒数第\(j+1\)个物品,那么就满足了贪心的原则,也方便了花费的计算。
具体的,我们可以这样进行花费提前计算:\(f_{i,j}=max(f_{i-1,j},f_{i-1,j-1}+v_{i}-(j-1)*w_i)\),第一种情况代表第\(i\)个物品不选,第二种情况代表选它作为倒数第\(j+1\)个物品,在倒序状态中,我们实际上以及计算了它未来的影响:减少了最后\(j-1\)个物品\(w_i\)的价值。
所以,对于有未来影响的\(dp\),我们可以使用花费提前计算的方法,当然,使用花费提前计算的方法通常还配合倒序的状态来使用。
\(Code:\)
#include <bits/stdc++.h> using namespace std; inline void read(int &k) { int x=0,w=0;char ch; while (!isdigit(ch)) w |= ch=='-' , ch = getchar(); while (isdigit(ch)) x = (x<<3) + (x<<1) + (ch^48) , ch=getchar(); k = ( w ? -x : x ); } const int N=5020; int n,ans,f ; struct product { int v,w; }a ; inline bool compare(product p1,product p2) { return p1.w > p2.w; } inline void input(void) { read(n); for (int i=1;i<=n;i++) read(a[i].v) , read(a[i].w); } inline void dp(void) { sort(a+1,a+n+1,compare); for (int i=1;i<=n;i++) { for (int j=1;j<=n;j++) { f[i][j] = max(f[i-1][j],f[i-1][j-1]+a[i].v-(j-1)*a[i].w); if (i==n) ans = max(ans,f[i][j]); } } } int main(void) { freopen("value.in","r",stdin); freopen("value.out","w",stdout); input(); dp(); printf("%d\n",ans); return 0; }
- 有关动态规划(主要是数位DP)的一点讨论
- 动态规划若干优化 & 集训部分总结
- 讨论动态规划的优点 - 相比于穷举法 - (以最短路径为例)
- 杭电acm上有关动态规划思想的习题
- 动态规划的优化技巧
- 【算法学习笔记】81.动态规划 分类讨论 SJTU OJ 1075 括号匹配升级
- 有关动态规划的一些理解
- POJ 有关动态规划的题目
- NYOJ 题目625 笨蛋的难题(二)(动态规划,技巧)
- 动态规划: 套路之推导公式 黑暗字符串 牛客
- 动态规划入门-数字三角形(从朴素递归到各种优化)
- 一本通上的某动态规划题:糖果(openjudge->NOI->2.6->2989)
- 动态规划之最大子段和问题
- 【暑假】[深入动态规划]UVa 10618 Fun Game
- 最长公共子序列求解:递归与动态规划方法
- 【暑假】[深入动态规划]UVa 10618 The Bookcase
- 动态规划之矩阵连乘
- 【hdu4283】区间动态规划问题
- 动态规划——矩形嵌套问题
- 动态规划总结之 LIS ,LCS