区间DP
概念
以区间长度作为阶段,使用区间的端点描述每个维度(其实就是dp数组的维度)。在区间DP中,一个状态由若干个比它更小且包含于它的区间所代表的区间转移而来,因此区间DP的决策常常是划分区间的方法。
例题
合并石子
难度:普及+/提高
题目描述
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 nnn 及每堆的石子数,并进行如下计算:
- 选择一种合并石子的方案,使得做 n−1n-1n−1 次合并得分总和最大。
- 选择一种合并石子的方案,使得做 n−1n-1n−1 次合并得分总和最小。
输入
输入第一行一个整数 n,表示有 n 堆石子。
第二行 n 个整数,表示每堆石子的数量。
输出
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
提示
对于100%的数据,有1<=n<=200。
思路
其实这就是一道简单的模板题,只用处理一下环的问题,将环剪成链,再分别求出MAX与MIN即可。
#include<bits/stdc++.h> #define fu(i,a,b) for(int i=a;i<=b;i++) #define fd(i,a,b) for(int i=a;i>=b;i--) using namespace std; int read() { int x=0,k=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') k=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x*k; } int n,se[405],qe[405],fx[405][405],fn[405][405],l,r,maxx,minn; int main() { n=read(); memset(qe,0,sizeof(qe)); memset(fx,0,sizeof(fx)); memset(fn,0,sizeof(fn)); fu(i,1,n) { se[i]=read(); se[n+i]=se[i]; qe[i]=qe[i-1]+se[i]; fx[i][i]=0; fn[i][i]=0; } fu(i,n+1,2*n) { qe[i]=qe[i-1]+se[i]; fx[i][i]=fn[i][i]=0; } fu(jie,2,n) fu(l,1,2*n-jie+1) { r=l+jie-1; fx[l][r]=-1; fn[l][r]=0x7fffffff; fu(k,l,r-1) { fx[l][r]=max(fx[l][r],fx[l][k]+fx[k+1][r]); fn[l][r]=min(fn[l][r],fn[l][k]+fn[k+1][r]); } fx[l][r]+=qe[r]-qe[l-1]; fn[l][r]+=qe[r]-qe[l-1]; } maxx=-1;minn=0x7fffffff; fu(i,1,n) { maxx=max(maxx,fx[i][i+n-1]); minn=min(minn,fn[i][i+n-1]); } cout<<minn<<" "<<maxx; return 0; }
能量项链
难度:普及+/提高
题目描述
在Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为(Mars单位),新产生的珠子的头标记为m,尾标记为n。
需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第j,k两颗珠子聚合后所释放的能量。则第4、1两颗珠子聚合后释放的能量为:
(4⊕1)=10*2*3=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:
((4⊕1)⊕2)⊕3)=10*2*3+10*3*5+10*5*10=710。
输入
每组输入数据的第一行是一个正整数N(4≤N≤100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1≤i≤N),当i<N时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
输出
每组输出只有一行,是一个正整数E(E≤2.1*109),为一个最优聚合顺序所释放的总能量。
思路
这道题表述成链就一定有它的道理,但它实际上还是环,我们用tou存头标记,用wei存尾标记,截环为链,即可。
#include<bits/stdc++.h> #define fu(i,a,b) for(int i=a;i<=b;i++) #define fd(i,a,b) for(int i=a;i>=b;i--) using namespace std; int read() { int x=0,k=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') k=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x*k; } int tou[205],wei[205],f[205][205]; int E=0,n; int main() { n=read(); fu(i,1,n) { tou[i]=read(); tou[n+i]=tou[i]; } fu(i,1,2*n-1) { wei[i]=tou[i+1]; f[i][i]=0; } wei[2*n]=tou[1]; fu(jie,1,n-1) fu(l,1,2*n-jie) { int r=l+jie; fu(k,l,r-1) f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+tou[l]*wei[k]*wei[r]); } fu(i,1,n) E=max(E,f[i][i+n-1]); cout<<E; return 0; }
凸多边形的划分
难度:提高+/省选-
题目描述
给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 N-2 个互不相交的三角形,试求这些三角形顶点的权值乘积和至少为多少。
输入
输入第一行为顶点数 N
第二行依次为顶点 1 至顶点 N 的权值。
输出
输出仅一行,为这些三角形顶点的权值乘积和的最小值。
提示
对于 100% 的数据,有 N<= 50,每个点权值小于 10^9。
思路
#include<bits/stdc++.h> #define fu(i,a,b) for(int i=a;i<=b;i++) #define fd(i,a,b) for(int i=a;i>=b;i--) #define ll long long using namespace std; int read() { int x=0,k=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') k=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x*k; } ll f[105][105][105],a[105]; ll s1[105],s2[105],s3[105]; int n; void mark(ll c[]) { fu(i,1,c[0]) { c[i+1]+=c[i]/10000; c[i]%=10000; } while(c[c[0]+1]) { c[0]++; c[c[0]+1]+=c[c[0]]/10000; c[c[0]]%=10000; } } void mul(ll a1,ll a2,ll a3,ll c[]) { c[0]=c[1]=1; fu(i,1,c[0]) { c[i]*=a1; } mark(c); fu(i,1,c[0]) { c[i]*=a2; } mark(c); fu(i,1,c[0]) { c[i]*=a3; } mark(c); } void add(ll a[],ll b[],ll c[]) { if(a[0]>b[0]) c[0]=a[0]; else c[0]=b[0]; fu(i,1,c[0]) c[i]=a[i]+b[i]; mark(c); } int ce(ll a[],ll b[]) { if(a[0]<b[0]) return 0; if(a[0]>b[0]) return 1; fd(i,a[0],1) if(a[i]<b[i]) return 0; else if(a[i]>b[i]) return 1; return 0; } void p() { int i; cout<<f[1][n][f[1][n][0]]; fd(i,f[1][n][0]-1,1) { cout<<f[1][n][i]/1000; cout<<f[1][n][i]/100%10; cout<<f[1][n][i]/10%10; cout<<f[1][n][i]%10; } cout<<endl; } int main() { n=read(); fu(i,1,n) a[i]=read(); fu(i,1,n) fu(j,1,n) f[i][j][0]=0; fu(jie,2,n-1) fu(i,1,n-jie) { int j=i+jie; f[i][j][0]=60; fu(k,i+1,j-1) { memset(s1,0,sizeof(s1)); memset(s2,0,sizeof(s2)); memset(s3,0,sizeof(s3)); mul(a[i],a[k],a[j],s1); add(f[i][k],f[k][j],s2); add(s1,s2,s3); if(ce(f[i][j],s3)) memcpy(f[i][j],s3,sizeof(s3)); } } p(); return 0; }
优化
倍增思想(注意是思想)
例题:LG-P3147[USACO16OPEN]262144与LG-P3146[USACO16OPEN]248
解析
一维的2048换了个规则:两数合并结果+1。两题的不同在于:数据范围不同;这难道不是调整数组大小就能解决的事么?但是USACO的出题人不会傻到同一场出两道相同的题,
于是我们开始做数据范围小的题,直接套板子,若两个区间数值相同就合并,再用个ans取最大值。
#include<bits/stdc++.h> #define fu(i,a,b) for(int i=a;i<=b;i++) #define fd(i,a,b) for(int i=a;i>=b;i--) using namespace std; int read() { int x=0,k=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') k=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x*k; } int n,f[250][250],ans=-1; int main() { n=read(); fu(i,1,n) { int op; op=read(); if(ans<op) ans=op; f[i][i]=op; } fu(jie,2,n) { fu(i,1,n-jie+1) { int j=i+jie-1; fu(k,i,j-1) { if(f[i][k]==f[k+1][j]) f[i][j]=max(f[i][j],f[i][k]+1); ans=max(ans,f[i][j]); } } } cout<<ans; return 0; }
接下来是n<=262144,咦,怎么跟题目一样,一定有不可告人的秘密,2的18次方等于262144,倍增思想石锤。
设 f[ i ][ j ] 表示位置在 j 的数向右合并至 f[ i ][ j ] 得到 i ,那么在 j 处的数合并至 f[ i-1 ][ j ] 得到 i-1 ,便有在 f[ i-1 ][ j ] 处的数合并至 f [ i-1 ][ f[ i-1 ][ j ] ] 得到 i ;
出现了,转移方程!
#include<bits/stdc++.h> #define fu(i,a,b) for(int i=a;i<=b;i++) #define fd(i,a,b) for(int i=a;i>=b;i--) using namespace std; int read() { int x=0,k=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') k=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x*k; } int n,f[61][270000],ans; int main() { n=read(); for(int i=1;i<=n;i++) { int op; op=read(); f[op][i]=i+1; } for(int two=2;two<=58;two++) for(int t=1;t<=n;t++) { if(f[two][t]==0) f[two][t]=f[two-1][f[two-1][t]]; if(f[two][t]!=0) ans=two; //cout<<ans<<endl; } cout<<ans; return 0; }