动态规划 之 区间DP练习

前言

\(Loj\) 放上了那么多《信息学奥赛一本通》上的题(虽然我并没有这本书),我要给它点一个大大的赞 _
以后分类刷题不愁啦!


正文

那就一道道说吧。

石子合并

\(n\) 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
\(n−1\) 次合并后得分总和的最大值和最小值。
\(n \leq 200\)

首先注意到“绕圆形排放”,那么有一个经典的技巧就是在 \(1 \sim n\) 后再接一个 \(1 \sim n\) ,相当于把圆拆成一条直线
接着就是经典的区间 \(DP\) ,方程 \(f[i][j]=\mathop{\max}\limits_{k=i}^{j-1} \{f[i][k]+f[k+1][j]+sum[i][j] \}\),最小值亦然
\(sum[i][j]\)为第 \(i\) 堆到第 \(j\) 堆石子数之和,可以用前缀和算出来。
注意转移的顺序为区间大小从小至大。

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 405;

int n;
int f[N][N],g[N][N],s[N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) 
		scanf("%d",&f[i][i]),f[i+n][i+n]=f[i][i];
	for(int i=1;i<=n*2;i++) 
		s[i]=s[i-1]+f[i][i],f[i][i]=0;
	
	for(int i=2;i<=n;i++)
		for(int j=1;j+i-1<=n*2;j++){
			g[j][i+j-1]=2*1e9;
			for(int k=j;k<j+i-1;k++){
				f[j][i+j-1]=max(f[j][i+j-1],f[j][k]+f[k+1][i+j-1]+s[i+j-1]-s[j-1]);
				g[j][i+j-1]=min(g[j][i+j-1],g[j][k]+g[k+1][i+j-1]+s[i+j-1]-s[j-1]);
			}
		}
	
	int Max=0,Min=2*1e9;
	for(int i=1;i<=n;i++)
		Max=max(Max,f[i][i+n-1]),Min=min(Min,g[i][i+n-1]);
	printf("%d\n%d",Min,Max);
	
	return 0;
}

能量项链

在项链上有 \(N\) 颗能量珠。能量珠是一颗有头标记和尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记必定等于后一颗珠子的头标记。如果一颗能量珠头标记为 \(m\),尾标记为 \(r\),后一颗能量珠头标记为 \(r\),尾标记为 \(n\),则聚合后释放出 \(m \times r \times n\) 单位的能量,新珠子头标记为 \(m\),尾标记为 \(n\)
项链上有 \(n\) 颗珠子,相邻两颗珠子可以合并成一个,合并同时会放出一定的能量,不同珠子合并放出能量不相同,请问按怎样的次序合并才能使得释放的能量最多?
\(n \leq 100\)

与第一题套路差不多,这个题所有能量珠也是围成一个圈的(吐槽一下,这个事情原题中说的很不清楚)
所以也是 \(1\sim n\) 后再接 \(1 \sim n\)
转移方程也很套路,\(f[i][j]=\mathop{\max}\limits_{k=i}^{j-1} \{ f[i][k]+f[k+1][j]+w[i] \times w[k] \times w[j] \}\)

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 205;
typedef long long ll;

int n;
ll w[N],f[N][N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&w[i]),w[i+n]=w[i];
	
	for(int i=3;i<=n+1;i++)
		for(int j=1;j+i-1<=2*n;j++)
			for(int k=j+1;k<j+i-1;k++)
				f[j][j+i-1]=max(f[j][j+i-1],f[j][k]+f[k][j+i-1]+w[j]*w[j+i-1]*w[k]);
	
	ll ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,f[i][i+n]);
	printf("%lld\n",ans);
	
	return 0;
}

凸多边形的划分

给定一个具有 \(N\) 个顶点的凸多边形,将顶点从 \(1\)\(N\) 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 \(N−2\) 个互不相交的三角形,试求这些三角形顶点的权值乘积和至少为多少。
\(N \leq 50\) ,每个点权值 \(\leq 10^9\)

这是一道好题!!(因为我居然想了好半天没想出来,最后看的题解【捂脸】)
之前没想到是因为不知道怎么让区间连续起来,有些三角形三个顶点都不连续……
后来才意识到,当前多边形每一条边都一定在一个三角形中,故只需要拿住一条边,再选一个点作为三角形另一个顶点,然后就会把多边形分为两个部分,而这两个部分的多边形顶点是连续的。好巧好巧!
多边形定点是连成一个圈的,故也要在 \(1 \sim n\) 后接 \(1 \sim n\)
方程 \(f[i][j]=\mathop{\max}\limits_{k=i+1}^{j-1} \{ f[i][k]+f[k][j]+w[i] \times w[k] \times w[j] \}\)
坑人的是要写高精度 \(qwq\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 55*2;
const int SZ =1000;

struct Bign{
	int s[15],len;
	Bign() { len=0; memset(s,0,sizeof(s)); }
	void print(){
		printf("%d",s[len-1]);
		for(int i=len-2;i>=0;i--) {
			if(s[i]<10) printf("0");
			if(s[i]<100) printf("0");
			printf("%d",s[i]);
		}
		printf("\n");
	}
	Bign operator = (int x) {
		if(x==0) s[len++]=0;
		while(x!=0) s[len++]=x%SZ,x/=SZ;
		return *this;
	}
	Bign operator + (const Bign &b) const {
		Bign c;
		for(int i=0,g=0;;i++){
			if(i>=len && i>=b.len && g==0) break;
			c.s[c.len++]=(s[i]+b.s[i]+g)%SZ;
			g=(s[i]+b.s[i]+g)/SZ;
		}
		return c;
	}
	Bign operator * (const Bign &b) const {
		Bign c;
		for(int i=0;i<len;i++)
			for(int j=0;j<b.len;j++) c.s[i+j]+=s[i]*b.s[j];
		c.len=len+b.len-1; /**/
		for(int i=0,g=0;;i++){
			if(i>=c.len && c.s[i]==0 && g==0) { c.len=i; break; }
			g=c.s[i]+g;
			c.s[i]=g%SZ;
			g/=SZ;
		}
		return c;
	}
	bool operator > (const Bign &b) const {
		if(len!=b.len) return len>b.len;
		for(int i=len-1;i>=0;i--)
			if(s[i]!=b.s[i]) return s[i]>b.s[i];
		return false;
	}
}w[N],f[N][N],MAX; 
int n;

int main()
{
	int x;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&x),w[i]=x,w[i+n]=x;
	
	MAX.len=15; MAX.s[14]=1;
	for(int i=1;i<n*2;i++) f[i][i+1]=(x=0);
	for(int i=3;i<=n;i++)
		for(int j=1;j+i-1<=n*2;j++){
			f[j][i+j-1]=MAX;
			for(int k=j+1;k<j+i-1;k++)
				if(f[j][i+j-1]>f[j][k]+f[k][i+j-1]+w[j]*w[k]*w[i+j-1]) 
					f[j][i+j-1]=f[j][k]+f[k][i+j-1]+w[j]*w[k]*w[i+j-1];
		}
	
	Bign m=MAX;
	for(int i=1;i<=n;i++) if(m>f[i][i+n-1]) m=f[i][i+n-1];
	m.print();
	
	return 0;
}

括号配对

\(BE\) 中有一类被称为 \(GBE\)
以下是 \(GBE\) 的定义:
1.空表达式是 \(GBE\)
2.如果表达式 \(A\)\(GBE\),则 \([A]\)\((A)\) 都是 \(GBE\)
3.如果 \(A\)\(B\) 都是 \(GBE\),那么 \(AB\)\(GBE\)
给定 \(BE\) ,问最少添加多少字符可将其变为 \(GBE\)
字符串长度小于100

这个就是很常规的区间\(DP\),感觉没什么好说的
对于 \(f[i][j]\) ,先讨论 \(s[i]\)\(s[j]\) 是否可凑成一对中括号或小括号,如果可以的话 \(f[i][j]=f[i+1][j-1]\)
接着 \(f[i][j]=\mathop{\max}\limits_{k=i}^{j-1} \{ f[i][k]+f[k+1][j]\}\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>

#define INF 100007

using namespace std;

const int N = 105;

int n;
int f[N][N];
char s[N];

int main()
{
	scanf("%s",s);
	n=strlen(s);
	
	for(int i=0;i<n;i++) 
		if(s[i]=='(' || s[i]==')' || s[i]=='[' || s[i]==']') f[i][i]=1;
	for(int i=2;i<=n;i++)
		for(int j=0;j+i-1<n;j++){
			f[j][i+j-1]=INF;
			if(s[j]=='(' && s[i+j-1]==')') f[j][i+j-1]=min(f[j][i+j-1],f[j+1][i+j-2]);
			if(s[j]=='[' && s[i+j-1]==']') f[j][i+j-1]=min(f[j][i+j-1],f[j+1][i+j-2]);
			for(int k=j;k<i+j-1;k++)
				f[j][i+j-1]=min(f[j][i+j-1],f[j][k]+f[k+1][i+j-1]);
		}
	printf("%d\n",f[0][n-1]);
	
	return 0;
}

分离与合体

原题太长了我要吐槽!!!
杜神牛造了 \(n\) 个区域,他们紧邻着排成一行,编号 \(1…n\) 。在每个区域里都放着一把 \(OI\) 界的金钥匙,每一把都有一定的价值。
一开始 \(LYD\) 可以选择 \(1…n−1\) 中的任何一个区域进入,我们不妨把这个区域记为 \(k\)。进入后 \(LYD\) 会在 \(k\) 区域发生分离,从而分离成两个小 \(LYD\)。分离完成的同时会有一面墙在 \(k\) 区域和 \(k+1\) 区域间升起,从而把 \(1…k\)\(k+1…n\) 阻断成两个独立的区间,并在各自区间内任选除区间末尾之外的任意一个区域再次发生分离,这样就有了四个小小 \(LYD\)……重复以上所叙述的分离,直到每个小 \(LYD\) 发现自己所在的区间只剩下了一个区域,那么他们就可以抱起自己梦寐以求的 \(OI\) 金钥匙。
但是 \(LYD\) 不能就分成这么多个个体存在于世界上,这些小 $ LYD$ 还会再合体,合体的小 $ LYD$ 所在区间中间的墙会消失。合体会获得 (合并后所在区间左右端区域里金钥匙价值之和)\(\times\) (之前分离的时候所在区域的金钥匙价值)。
\(LYD\) 请你编程求出最终可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。
\(n \leq 300\)

感觉这题难点在于耐下心把题看完……
方程 \(f[i][j]=\mathop{\max}\limits_{k=i}^{j-1} \{ f[i][k]+f[k+1][j]+(a[i]+a[j]) \times a[k] \}\)
因为最后要输出方案,所以每个区域要记录下使 \(f[i][j]\) 取到 \(max\)\(k\)

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 305;
typedef long long ll;
typedef pair<int,int> P;

int n;
ll a[N],f[N][N];
int g[N][N];

int head,tail;
P que[N*N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	
	for(int i=2;i<=n;i++)
		for(int j=1;j+i-1<=n;j++)
			for(int k=j;k<j+i-1;k++)
				if(f[j][i+j-1]<f[j][k]+f[k+1][i+j-1]+(a[j]+a[i+j-1])*a[k])
					f[j][i+j-1]=f[j][k]+f[k+1][i+j-1]+(a[j]+a[i+j-1])*a[k],g[j][i+j-1]=k;
	
	printf("%lld\n",f[1][n]);
	que[tail++]=P(1,n);
	while(head<tail){
		int x=que[head].first,y=que[head++].second;
		if(x==y) continue;
		if(x!=1 || y!=n) printf(" %d",g[x][y]);
		else printf("%d",g[x][y]);
		que[tail++]=P(x,g[x][y]);
		que[tail++]=P(g[x][y]+1,y);
	}
	
	return 0;
}

矩阵取数游戏

对于给定的 \(n \times m\) 的矩阵,矩阵中每个元素 \(a_{i,j}\) 均为非负整数。游戏规则如下:
1.每次取数时必须从每行各取走一个元素,共 \(n\) 个,\(m\) 次取完所有元素。
2.每次取走的各个元素只能是该元素所在行行首或行尾。
3.每次取数都有一个的分值,为每行取数得分之和,每行取数得分=被取走元素值 \(\times 2^i\),其中 \(i\) 表示第 \(i\) 次取数,从 1 开始计数。
4.游戏结束时,总得分为 \(m\) 次取数得分之和。
求取数后的最大得分。\(n,m \leq 80, a_{i,j} \leq 10^3\)

这个也没啥难的,每行一次区间\(dp\)
方程 \(f[i][j]=max \{ f[i][j-1] +a[j] \times 2^{m-(j-i+1)+1} , f[i+1][j] +a[i] \times 2^{m-(j-i+1)+1} \}\)
恶心的是又要加高精度。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 85;
const int SZ = 10000;

struct Bign{
	int s[10],len;
	Bign() { len=0; memset(s,0,sizeof(s)); }
	Bign operator = (int x){
		len=0; memset(s,0,sizeof(s));
		if(x==0) s[len++]=0;
		while(x!=0) s[len++]=x%SZ,x/=SZ;
		return *this;
	}
	Bign operator + (const Bign &b) const{
		Bign c;
		for(int i=0,g=0;;i++){
			if(i>=len && i>=b.len && g==0) break;
			c.s[c.len++]=(s[i]+b.s[i]+g)%SZ;
			g=(s[i]+b.s[i]+g)/SZ;
		}
		return c;
	}
	Bign operator * (const int &x) const{
		Bign c;
		for(int i=0,g=0;;i++){
			if(i>=len && g==0) { c.len=i; break; }
			g=g+x*s[i];
			c.s[i]=g%SZ;
			g/=SZ;
		}
		return c;
	}
	bool operator < (const Bign &b) const{
		if(len!=b.len) return len<b.len;
		for(int i=len-1;i>=0;i--)
			if(s[i]!=b.s[i]) return s[i]<b.s[i];
		return false;
	}
	void print(){
		printf("%d",s[len-1]);
		for(int i=len-2;i>=0;i--){
			if(s[i]<10) printf("0");
			if(s[i]<100) printf("0");
			if(s[i]<1000) printf("0");
			printf("%d",s[i]);
		}
		printf("\n");
	}
}mod[N],f[N][N],ans;

int n,m;
int a[N];

int main()
{
	scanf("%d%d",&n,&m);
	
	mod[0]=1;
	for(int i=1;i<=m;i++) mod[i]=mod[i-1]*2;
	
	ans=0;
	for(int x=1;x<=n;x++){
		for(int y=1;y<=m;y++) 
			scanf("%d",&a[y]),f[y][y]=mod[m]*a[y];
		
		for(int i=2;i<=m;i++)
			for(int j=1;j+i-1<=m;j++){
				f[j][i+j-1]=0;
				if(f[j][i+j-1]<f[j][i+j-2]+mod[m-i+1]*a[i+j-1])
					f[j][i+j-1]=f[j][i+j-2]+mod[m-i+1]*a[i+j-1];
				if(f[j][i+j-1]<f[j+1][i+j-1]+mod[m-i+1]*a[j])
					f[j][i+j-1]=f[j+1][i+j-1]+mod[m-i+1]*a[j];
			}
		ans=ans+f[1][m];
	}
	ans.print();
	
	return 0;
}
posted @ 2018-10-11 09:52  秋千旁的蜂蝶~  阅读(559)  评论(0编辑  收藏  举报