『简单dp测试题解』

<更新提示>

<第一次更新> 这一次组织了一场\(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

\(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[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\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;
}

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

\(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[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\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[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\n",ans);
	return 0;
}

<后记>

posted @ 2019-04-28 20:59  Parsnip  阅读(638)  评论(0编辑  收藏  举报