dp day2/3-区间dp

区间dp

区间dp一般都是考虑对于每段区间,最优值都是由几段更小区间的最优值得到,是分治思想的一种应用,

将一个区间问题不断划分为更小的区间直至一个元素组成的区间,枚举他们的组合 ,求合并后的最优值。

道理我都懂,但是做了几题之后,发现最难的一点是要看出这题是个区间dp......

这需要一些转化,需要积累......

然后是要找到阶段、状态和状态转移方程。

洛谷p1880石子合并

环形的区间dp,是一个模版,具体看代码吧......

#include<bits/stdc++.h>
using namespace std;
int n,a[210]={0},s[210][210]={0},ma=0,mi=1000000,sum[210]={0};
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
	    scanf("%d",&a[i]);
	    a[i+n]=a[i];//环形,所以乘二
	}
	for(int i=1;i<=n*2;i++)sum[i]=sum[i-1]+a[i];//前缀和 
	
	for(int i=1;i<=n;i++)//先搜最大值(否则就听取WA声一片啦)i表示合并的区间长度
	for(int j=1;j<=n*2;j++)//j表示区间起点
	{
	    int end=j+i-1;if(end>2*n)break;
	    for(int k=j;k<end;k++)//枚举断开点
	    s[j][end]=max(s[j][end],s[j][k]+s[k+1][end]+sum[end]-sum[j-1]);
	}//sum即这次移动所加值 
	for(int i=1;i<=n;i++)if(s[i][i+n-1]>ma)ma=s[i][i+n-1];
	
	for(int i=1;i<=n;i++)//最小值 
	for(int j=1;j<=n*2;j++)
	{
	    int end=j+i-1;if(end>2*n)break;
	    for(int k=j;k<end;k++)
	    s[j][end]=min(s[j][end],s[j][k]+s[k+1][end]+sum[end]-sum[j-1]);
	}
	for(int i=1;i<=n;i++)if(s[i][i+n-1]<mi)mi=s[i][i+n-1];
	printf("%d\n%d",mi,ma);
	return 0;
}
洛谷p1063能量项链

也是区间dp,思想很相似

#include<bits/stdc++.h>
using namespace std;
long long a[210],t,s[210][210]={0},ma=0,mi=1000000,sum[210]={0};
int main()
{
	scanf("%d",&t);
	for(int i=1;i<=t;i++)
	{
	    scanf("%d",&a[i]);
	    a[i+t]=a[i];
	}
	for(int i=1;i<=t;i++)//依旧枚举区间长度
	for(int j=1;j<=t*2;j++)//枚举起始点
	{
	    int end=j+i-1;if(end>2*t)break;
	    for(int k=j;k<end;k++)//枚举断开点
	    s[j][end]=max(s[j][end],s[j][k]+s[k+1][end]+a[j]*a[k+1]*a[end+1]);//选择最优解
	}
	for(int i=1;i<=t;i++)if(s[i][i+t-1]>ma)ma=s[i][i+t-1];
	printf("%lld",ma);
	return 0;
}

洛谷p1220关路灯

已经关闭的灯一定在一个连续区间里,这很显然。如果a和b是不连续的两盏灯,被灯c分开,那么关了a再去关b的时候可以顺带把c关掉。

然后发现,在关闭了一个连续的区间的灯之后,人一定在端点。

所以用s[i][j][0]表示关第i盏到第j盏路灯后人处于第i盏路灯时的最小值,s[i][j][1]表示人处于第j盏灯时的最小值。

就可以区间dp啦。

#include<bits/stdc++.h>
using namespace std;
int s[60][60][2],n,c;
struct light
{
	int p,w;
}l[60];
inline void init()
{
	scanf("%d%d",&n,&c);
	l[0].w=0;
	for(int i=1;i<=n;i++)
	{
	    scanf("%d%d",&l[i].p,&l[i].w);
	    l[i].w+=l[i-1].w;//前缀和
	}
	return;
}

inline void dp()
{
	for(int i=1;i<=n;i++)
	s[i][i][0]=s[i][i][1]=l[n].w*abs(l[i].p-l[c].p);//初始值 
	for(int len=2;len<=n;len++)
	for(int i=1;i<=n-len+1;i++)
	{
		int j=i+len-1;//最后关第i盏灯则之前已关第i+1至第j盏灯,最后关第j盏灯反之
		s[i][j][0]=min(s[i+1][j][0]+abs(l[i+1].p-l[i].p)*(l[i].w+l[n].w-l[j].w),//加上的为关灯途中没关的灯所耗的电能
		               s[i+1][j][1]+abs(l[j].p-l[i].p)*(l[i].w+l[n].w-l[j].w));
		s[i][j][1]=min(s[i][j-1][1]+abs(l[j].p-l[j-1].p)*(l[i-1].w+l[n].w-l[j-1].w),
		               s[i][j-1][0]+abs(l[j].p-l[i].p)*(l[i-1].w+l[n].w-l[j-1].w));
	}
	return;
}

int main()
{
	init();
	dp();
	if(s[1][n][0]<s[1][n][1])
	printf("%d",s[1][n][0]);
	else printf("%d",s[1][n][1]);
	return 0;
}
洛谷p1622释放囚犯

正着做死活做不出,参考题解中所说,将这个过程反过来,把要释放的人视作断点,将p个人分成q+1个区间,求合并区间至一个区间所需最小值,即可视为合并石子,突然通悟......

#include<bits/stdc++.h>
using namespace std;
int s[110][110]={0},p,q,a[110]={0},sum[110]={0};
inline void init()
{
	scanf("%d%d",&p,&q);
	for(int i=1;i<=q;i++)scanf("%d",&a[i]);
	a[0]=0;a[++q]=p+1;
        sort(a,a+q+1);
	return;
}

int main()
{
    init();
    for(int i=1;i<=q;i++)
    sum[i]=a[i]-a[i-1]-1+sum[i-1];//前缀和,将问题转换为求几堆石子合并的最小值
    for(int k=2;k<=q;k++)
    for(int i=1;i<=q-k+1;i++)
    {
        int j=i+k-1;
        for(int p=i;p<j;p++)
        if(!s[i][j]||s[i][j]>s[i][p]+s[p+1][j]+sum[j]-sum[i-1]+j-i-1)//注意j-i+1,是指合并时几个还未释放的人
        s[i][j]=s[i][p]+s[p+1][j]+sum[j]-sum[i-1]+j-i-1;
    }
    printf("%d",s[1][q]);
    return 0;
}

洛谷p3205合唱队伍

先将问题转化为求排成末状态的不同的方案数。

对于最终序列的一个区间[i,j],最后一个添加的只可能是第i个人或者第j个人。如果最后一个是i,则对于这个区间,a[i]<a[i+1]或者a[i]<a[j]。如果最后一个是j,则对于这个区间a[j]>a[j-1]或者a[j]>a[i]。

#include<bits/stdc++.h>
using namespace std;
int n,s[1010][1010][2]={0},a[1010]={0};s[i][j][0]表示排出[i,j]且最后一个人从左边排入的方案数,s[i][j][1]表示最后一人从右边排入
const int mod=19650827;
inline void init()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)s[i][i][0]=1;//s[i][i][1]不能也存1,否则结果会double
	return;
}

inline void dp()
{
	for(int len=2;len<=n;len++)
	for(int i=1;i<=n-len+1;i++)
	{
		int j=i+len-1;
		if(a[i]<a[i+1])s[i][j][0]+=s[i+1][j][0];//如果可从左侧排入人
		if(a[i]<a[j])s[i][j][0]+=s[i+1][j][1];
		if(a[j]>a[i])s[i][j][1]+=s[i][j-1][0];//如果可从右侧排入人
		if(a[j]>a[j-1])s[i][j][1]+=s[i][j-1][1];
		s[i][j][0]%=mod;
		s[i][j][1]%=mod;
	}
	return;
}

int main()
{
	init();
	dp();
	printf("%d",(s[1][n][0]+s[1][n][1])%mod);
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值