<更新提示>
<第一次更新> 这一次组织了一场(dp)的专项考试,出了好几道经典的简单(dp)套路题,特开一篇博客写一下题解。
<正文>
Tower(双向dp)
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
(nleq1000,mleq5*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[N][N],u[N][N],d[N][N];
pair < long long , long long > pos[N];
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
",val(x,pos[x].second));
else printf("%lld
",val(x,pos[x].first));
}
}
int main(void)
{
freopen("tower.in","r",stdin);
freopen("tower.out","w",stdout);
input();
dp();
solve();
return 0;
}
Market(DP换置)
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
(nleq300,mleq10^5,c_i,M_ileq10^9,v_ileq300,T_i,t_ileq300)
Solution
这是经典(dp)模型(0/1)背包的(dp)换置。
直观地考虑,这就是一道简单的(0/1)背包,但是这个背包的体积很大,不适合直接做。那么我们注意到数据中一个很好的限制:(v_ileq300),也就是说,我们可以考虑一种与价值有关的(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[N];
long long f[N][N*N];
struct market
{
int c,v,t;
}a[N];
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
",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
(nleq5000,v_i,w_ileq10^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[N][N];
struct product
{
int v,w;
}a[N];
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
",ans);
return 0;
}
<后记>