LOJ 一本通提高篇5.1区间类动态规划 例题+练习

2 篇文章 0 订阅

是最讨厌的DP啊/哭/,但是总考不是/大哭/,我有什么办法/哭死了/。

好吧,DP还好几种:区间、树形、数位..@$#%4*(我才没有骂人)切腹自尽。

其实区间DP的用法还是比较单一的?(至少下面的题是?)我还不是一样做不出来。


目录

#10147. 「一本通 5.1 例 1」石子合并

#10148. 「一本通 5.1 例 2」能量项链

#10149. 「一本通 5.1 例 3」凸多边形的划分

#10150. 「一本通 5.1 练习 1」括号配对

#10151. 「一本通 5.1 练习 2」分离与合体

#10152. 「一本通 5.1 练习 3」矩阵取数游戏


#10147. 「一本通 5.1 例 1」石子合并

题目

题目大意

将n堆石子绕圆形操场排放,要将石子有序地合并成一堆。

每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

选择一种合并石子的方案,使得做n-1次合并得分总和最大和最小。

对于100%的数据,有1<=n<=200。

题目分析

once upon a time,师兄好像讲过这道题和能量项链。

那时候我太弱了啥都不会一脸懵逼,傻逼ba我。

直接放师兄留下的PPT上的东西ba(讲的真的超级清楚了)

这个里面不是环形的,请先理解一下更方便后面环形的操作(大同小异)

定义

  • 定义sum[i]表示从第1堆石子到第i堆石子的石子数目和。
  • 定义f[i][j]表示将从第i堆到第j堆石子合并成一堆后的最大分数值。

状态转移方程

  • 很明显我们可以得出下面这个结论:
  • 第i堆到第j堆合并后的最大分值=第i堆到第k堆合并到的最大分值+第k+1堆到第j堆合并到的最大分值+第i堆到第j堆的石子数总和。
  • K是i到j中的某个数。
  • K是哪个数不重要,重要的是这个k能使合并完第i堆到第j堆的分数值最大。
  • 于是得出方程:f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1]

你需要的环形,i have an idea。

两个小小的不一样:

 简单的double。

  1. 定义sum[i][j]表示从第i堆石子到第j堆石子的石子数目和(i<=j)。
  2. 至于环形怎么处理嘛。枚举从每两堆之间分开,看看从哪里分开最后得到的分值最大。

举个栗子:三堆石子,石子数目分别为1,2,3

double之后:1,2,3,1,2,3

这样就ok啦(分别是红色的下划线的加粗的

你想要哪种就哪种。这样就可以处理环形的了。

你需要找的就是最大的f[i][i+石堆数量-1]

对了这道题还有点不一样就是既要求最大也要求最小。

当然都是一样的道理。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n,minn=1e9,maxx=0;
int a[410],sum[410][410];
int fmin[410][410],fmax[410][410];
//fmin记录最小,fmax记录最大 

int main()
{
	scanf("%d",&n);
	memset(fmin,63,sizeof(fmin));
	memset(fmax,0,sizeof(fmax));
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		a[i+n]=a[i];
	}
	for(int i=1;i<=2*n;i++) 
	{
		sum[i][i]=a[i];
		fmin[i][i]=fmax[i][i]=0;
		for(int j=i+1;j<=2*n;j++)
		{
			sum[i][j]=sum[i][j-1]+a[j];
		}
	}
	for(int c=2;c<=n;c++)//枚举长度 
	{
		for(int i=1;i<=2*n-c+1;i++)//开头 
		{
			int j=i+c-1;//结尾 
			for(int k=i;k<=j-1;k++)
			{
				fmin[i][j]=min(fmin[i][j],fmin[i][k]+fmin[k+1][j]+sum[i][j]);
				fmax[i][j]=max(fmax[i][j],fmax[i][k]+fmax[k+1][j]+sum[i][j]);
				if(c==n) 
				{
					minn=min(minn,fmin[i][j]);
					maxx=max(maxx,fmax[i][j]);
				}
			}
		}
	}
	printf("%d\n%d",minn,maxx);
	return 0;
}

 

#10148. 「一本通 5.1 例 2」能量项链

题目

题目大意

在项链上有n颗能量珠。

能量珠有头标记和尾标记,并且对于相邻的两颗珠子,前一颗珠子的尾标记等于后一颗珠子的头标记。

如果一颗能量珠头标记为m,尾标记为r,后一颗能量珠头标记为r,尾标记为n,则聚合后释放出m*r*n的能量。

新珠子头标记为m,尾标记为n。

一串项链上有n颗珠子,相邻两颗珠子可以合并成一个,设计一个聚合顺序使聚合后释放出的能量最大。

对于100%的数据,4<=n<=100。

题目分析

还是那个PPT

这题和合并石子是非常类似的。

唯一的区别在它是环形。

那么怎么办呢?

因为它是环形的,所以我们可以选择从某两个能量珠中把这个环形的项链断开,成为一条直线。

当然地,我们也要知道从哪个断口切开最后得到的分数最大啊?

方法还是枚举。

枚举从每两个相邻珠子之间切开,看看从哪里切开最后得到的分值最大。

怎么跟上一题一样a

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n,maxx=0;
int x[210],sum[210],f[210][210];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&x[i]); x[i+n]=x[i];
		sum[i-1]=x[i]; sum[i-1+n]=sum[i-1];
	}
	sum[2*n]=sum[n];
	for(int c=2;c<=n;c++)
	{
		for(int i=1;i<=2*n-c+1;i++)
		{
			int j=i+c-1;
			for(int k=i;k<=j-1;k++)
			{
				f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+x[i]*sum[k]*sum[j]);
				if(c==n) maxx=max(maxx,f[i][j]);
			}
		}
	}
	printf("%d",maxx);
	return 0;
}

 

#10149. 「一本通 5.1 例 3」凸多边形的划分

题目

题目大意

给定一个具有N个顶点的凸多边形,每个顶点的权值都是一个正整数。

将这个凸多边形划分成N-2个互不相交的三角形,求这些三角形顶点的权值乘积和至少为多少。

对于100%的数据,有N<=50,每个点权值小于10^9。

题目分析

为什么跟上面的不一样???你需要来个大拐弯!

画个图/滑稽/。

安利:https://blog.csdn.net/xuechen_gemgirl/article/details/80830106 图画的超级好看/赞/%%%

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n;
long long a[60],f[60][60];

int main()
{
	scanf("%d",&n);
	memset(f,63,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		f[i][i+1]=0;//相邻两个点组不成三角形 
	}
	for(int c=2;c<n;c++)
	{
		for(int i=1;i<=n-c;i++)
		{
			int j=i+c;
			for(int k=i+1;k<j;k++)
			{
				f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[k]*a[j]);
			}
		}
	}
	printf("%lld",f[1][n]);
	return 0;
}

 然后你需要加高精/害怕/,淦!

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n;
struct node{int l=0,a[1300];node(){memset(a,0,sizeof(a));}}a[60],b,f[60][60];

node cf(node x,node y)//高精度乘法 
{
	node z; z.l=x.l+y.l-1;
	for(int i=1;i<=x.l;i++)
	{
		for(int j=1;j<=y.l;j++)
		{
			z.a[i+j-1]+=x.a[i]*y.a[j];
		}
	}
	for(int i=1;i<=z.l;i++)
	{
		if(z.a[i]>9)
		{
			z.a[i+1]+=z.a[i]/10;
			z.a[i]%=10;
			if(i+1>z.l) z.l++;
		}
	}
	while(z.l>1&&z.a[z.l]==0) z.l--;
	return z;
}

node jf(node x,node y)//高精度加法 
{
	node z; z.l=max(x.l,y.l);
	if(x.l==0) return y;
	else if(y.l==0) return x;
	for(int i=1;i<=z.l;i++)
	{
		z.a[i]=x.a[i]+y.a[i]+z.a[i];
		if(z.a[i]>9)
		{
			z.a[i+1]++; z.a[i]%=10;
			if(i+1>z.l) z.l++;
		}
	}
	while(z.l>1&&z.a[z.l]==0) z.l--;
	return z;
}

int main()
{
	scanf("%d",&n);
	memset(f,63,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		long long x; scanf("%lld",&x);
		while(x>0)
		{
			int xx=x%10; x/=10;
			a[i].l++; a[i].a[a[i].l]=xx;
		}
		f[i][i+1].l=0;
	}
	for(int c=2;c<n;c++)
	{
		for(int i=1;i<=n-c;i++)
		{
			int j=i+c;
			for(int k=i+1;k<j;k++)
			{
				b=cf(a[i],a[k]);
				b=cf(b,a[j]);
				if(f[i][k].l>1e6) continue;
				if(f[k][j].l>1e6) continue;
				b=jf(b,f[i][k]); b=jf(b,f[k][j]);
				if(b.l<f[i][j].l) f[i][j]=b;
				else if(b.l==f[i][j].l)
				{
					for(int q=b.l;q>=1;q--)
					{
						if(b.a[q]<f[i][j].a[q])
						{
							f[i][j]=b;
							break;
						}
						else if(b.a[q]>f[i][j].a[q]) break;
					}
				}
			}
		}
	}
	for(int i=f[1][n].l;i>=1;i--) printf("%d",f[1][n].a[i]);
	return 0;
}

 

#10150. 「一本通 5.1 练习 1」括号配对

题目

题目大意

GBE的定义:

1、空表达式是GBE

2、如果表达式A是GBE,则[A]与(A)都是 GBE

3、如果A与B都是GBE,那么AB是GBE

给出一个BE,求至少添加多少字符能使这个BE成为GBE。

对于100%的数据,输入的字符串长度小于100。

题目分析

你只需要找配不上的,相应的你就知道你需要添加的啦。

一些解释放代码里了

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

char s[110];
int f[110][110];

int main()
{
	scanf("%s",s+1);
	int l=strlen(s+1);
	for(int i=1;i<=l;i++)
	{
		f[i][i]=1;
		f[i][i-1]=0; 
	}
	for(int c=2;c<=l;c++)
	{
		for(int i=1;i<=l-c+1;i++)//开头 
		{
			int j=i+c-1;//结尾 
			f[i][j]=0x3f3f3f3f;
			for(int k=i;k<=j;k++)
			{
				f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
				//不需要担心k和k+1配上了却没记上 
				//因为总会找到f[i][k+1]和f[k+1][j]的 
			}
			if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']'))//头和尾配上了 
			{
				f[i][j]=min(f[i][j],f[i+1][j-1]);//第16行派上用处了 
			}
		}
	}
	printf("%d",f[1][l]);//配不上的=需要添加的 
	return 0;
}

 

#10151. 「一本通 5.1 练习 2」分离与合体

题目

题目大意

n个区域里都放着一把金钥匙,每一把都有一定的价值。

可以选择1...n-1中的任何一个区域进入。

进入后会在k区域发生分离,并在各自区间内任选除区间末尾之外的任意一个区域再次发生分离……

重复以上所叙述的分离。

合体会获得(合并后所在区间左右端区域里金钥匙价值之和)*(之前分离的时候所在区域的金钥匙价值)。

求出最终可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。

对于20%的数据,n<=10;

对于40%的数据,n<=50;

对于100%的数据,n,ai<=300保证运算过程和结果不超过32位正整数范围。

题目分析

(合并后所在区间左右端区域里金钥匙价值之和)*(之前分离的时候所在区域的金钥匙价值)。

只要看这一句就好了。跟前面一样。我就说比较单一吧。

但是!!要输出顺序!!有人告诉我要暴力找?

吃水不忘挖井人。你得知道你从哪来,你才知道你要回哪去对吧。

比如:

你知道第一层的最优是在a处分离,

那么你可以分别去f[1][a]和f[a+1][n]找第二层你是从哪儿来的对吧,

然后这个在前面你找max的时候顺手记录一下就ok了对吧。

所以水到渠成,看代码。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

long long f[310][310];
int n,a[310],s[310][310],an[310][310],aa[310];

void solve(int l,int r,int c)//区间[l,r],c表示分了几次 
{
	if(!s[l][r]||l>=r) return ;//s没有标注说明l>=r 
	aa[c]++; an[c][aa[c]]=s[l][r];//记录第c层分了些什么 
	//然后找我的下一层分的是什么 
	solve(l,s[l][r],c+1);
	solve(s[l][r]+1,r,c+1);
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int c=2;c<=n;c++)
	{
		for(int i=1;i<=n-c+1;i++)
		{
			int j=i+c-1;
			for(int k=i;k<=j-1;k++)
			{
				long long t=f[i][j];
				f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+(a[i]+a[j])*a[k]);
				if(t!=f[i][j]) s[i][j]=k;//记录你从哪里来!(我从这里来/滑稽/) 
			}
		}
	}
	printf("%lld\n",f[1][n]);
	//跟前面的一模一样 
	solve(1,n,0);
	for(int i=0;i<n;i++)//分了n-1层(这个毋容置疑吧) 
	{
		for(int j=1;j<=aa[i];j++)//输出我当前层在哪里分了 
		{
			printf("%d ",an[i][j]);
		}
	}
	return 0;
}

 

#10152. 「一本通 5.1 练习 3」矩阵取数游戏

题目

题目大意

n*m的矩阵,矩阵中每个元素均为非负整数。

每次取数时必须从每行各取走一个元素,共n个,m次取完所有元素。

每次取走的各个元素只能是所在行的行首或行尾。

每次取数都有一个的分值,为每行取数得分之和。求出取数后的最大得分。

每行取数得分=被取走元素值*2^i,其中i表示第i次取数,从i开始计数。

对于60%的数据,1<=n,m<=30,答案不超过10^16;

对于100%的数据,1<=n,m<=80,0<=ai,aj<=1000。

题目分析

一头雾水?来个贪心。

肯定哪个小先取哪个啊,然后剩下打的跟2的更大次方乘。我又不傻。

高兴的敲。然后你(不才不是我我这么机智)发现你错了。

举个栗子:

1 5 1 10 3 3

你的贪心顺序:1 3 3 5 1 10

=1*2+3*4+3*8+5*16+1*32+10*64 =2+12+24+80+32+640 =790

       最佳的顺序:1 5 1 3 3 10

=1*2+5*4+1*8+3*16+3*32+10*64 =2+20+8+48+96+640 =814

然后你会发现这种栗子一抓一大把。

认命DP...不会从外面往里面搞是什么操作?好像(的确)不行。

从里面随便挑一个往外D呗。然后核心我讲完了。

还是很单一。还是不会。还不懂就瞧瞧代码。

代码

你猜猜2^80有多大??哈哈哈(按计算机)不吓你了。要用高精啊,好复杂

先放一个思路简明的没有高精的。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n,m,a[80][80];
long long k,kk=1,f[90][90],ans=0;

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);
			if(i==1) kk*=2;
		}
	}
	for(int q=1;q<=n;q++)//第q行 
	{
		memset(f,0,sizeof(f)); k=kk;
		for(int p=1;p<=m;p++) f[p][p]=a[q][p]*k;//最后一步的单个盈利 
		for(int c=2;c<=m;c++)
		{
			k/=2;
			for(int i=1;i<=m-c+1;i++)//头 
			{
				int j=i+c-1;//尾 
				f[i][j]=max(f[i][j],f[i+1][j]+a[q][i]*k);//你的上一个要么是左边 
				f[i][j]=max(f[i][j],f[i][j-1]+a[q][j]*k);//          要么是右边 
			}
		}
		ans+=f[1][m];//这一行的最大得分 
	}
	printf("%lld",ans);
	return 0;
}

高精来辣!

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n,m;
struct node{int l=0,a[110];node(){memset(a,0,sizeof(a));}}a[90][90],kk,c,k,f[90][90],aa,ans;

node cf(node x,node y)//高精度乘法 
{
	node z; z.l=x.l+y.l-1;
	for(int i=1;i<=x.l;i++)
	{
		for(int j=1;j<=y.l;j++)
		{
			z.a[i+j-1]+=x.a[i]*y.a[j];
		}
	}
	for(int i=1;i<=z.l;i++)
	{
		if(z.a[i]>9)
		{
			z.a[i+1]+=z.a[i]/10;
			z.a[i]%=10;
			if(i+1>z.l) z.l++;
		}
	}
	while(z.l>1&&z.a[z.l]==0) z.l--;
	return z;
}

node jf(node x,node y)//高精度加法 
{
	node z; z.l=max(x.l,y.l);
	for(int i=1;i<=z.l;i++)
	{
		z.a[i]=x.a[i]+y.a[i]+z.a[i];
		if(z.a[i]>9)
		{
			z.a[i+1]++; z.a[i]%=10;
			if(i+1>z.l) z.l++;
		}
	}
	while(z.l>1&&z.a[z.l]==0) z.l--;
	return z;
}

node chu(node x)//高精度除法(只限除以2) 
{
	node z; z.l=x.l;
	for(int i=x.l;i>=1;i--)
	{
		if(x.a[i]>0)
		{
			if(x.a[i]==1) x.a[i-1]+=10;
			else
			{
				z.a[i]=x.a[i]/2;
				if(x.a[i]%2) x.a[i-1]+=10;
			}
		}
	}
	while(z.l>1&&z.a[z.l]==0) z.l--;
	return z;
}

int main()
{
	scanf("%d%d",&n,&m);
	c.l=kk.l=1; kk.a[1]=1; c.a[1]=2;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			int x; scanf("%d",&x);
			while(x>0)
			{
				int xx=x%10; x/=10;
				a[i][j].l++; a[i][j].a[a[i][j].l]=xx;
			}
			if(i==1) kk=cf(kk,c);
		}
	}
	for(int q=1;q<=n;q++)
	{
		memset(f,0,sizeof(f)); k=kk;
		for(int p=1;p<=m;p++) f[p][p]=cf(a[q][p],k);
		for(int c=2;c<=m;c++)
		{
			k=chu(k);
			for(int i=1;i<=m-c+1;i++)
			{
				int j=i+c-1;
				aa=cf(a[q][i],k); aa=jf(aa,f[i+1][j]);
				if(aa.l>f[i][j].l) f[i][j]=aa;
				else if(aa.l==f[i][j].l)
				{
					for(int q=aa.l;q>=1;q--)
					{
						if(aa.a[q]<f[i][j].a[q])
						{
							f[i][j]=aa;
							break;
						}
						else if(aa.a[q]>f[i][j].a[q]) break;
					}
				}
				aa=cf(a[q][j],k); aa=jf(aa,f[i][j-1]);
				if(aa.l>f[i][j].l) f[i][j]=aa;
				else if(aa.l==f[i][j].l)
				{
					for(int q=aa.l;q>=1;q--)
					{
						if(aa.a[q]>f[i][j].a[q])
						{
							f[i][j]=aa;
							break;
						}
						else if(aa.a[q]<f[i][j].a[q]) break;
					}
				}
			}
		}
		ans=jf(ans,f[1][m]);
	}
	if(ans.l==0) printf("0");
	for(int i=ans.l;i>=1;i--) printf("%d",ans.a[i]);
	return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值