您的位置:首页 > 其它

『有关动态规划技巧与套路的若干讨论』

2019-04-28 20:59 92 查看

最近组织了一场\(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;
}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: