区间dp
区间dp
区间dp顾名思义就是根据区间使用动态规划,具体使用的方式要根据题目具体分析,这里只是一个大体的思路:首先枚举区间大小,从小到大枚举(这是第一层循环),再枚举区间(这是第二层循环),最后枚举得到此区间的方式,取最大值或最小值(这是第三层循环)。再循环结束后得到整个区间的最大值或最小值。其中,还有一种首尾相接的情况,那就将区间复制一遍,扩大一倍。
这几天做了关于区间dp的一些题目,在这里整理整理。
洛谷P1880 [NOI1995] 石子合并
题目描述
在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。
输入格式
数据的第 1 行是正整数 N,表示有 N 堆石子。
第 2 行有 N 个整数,第 i 个整数 a[i]表示第 i 堆石子的个数。
输出格式
输出共 2 行,第 1 行为最小得分,第 2 行为最大得分。
输入输出样例
1≤N≤100,0≤a[i]≤20。
这道题是一个典型的首尾相接的区间dp题目,而得到新区间的方式就是将这个区间分为两个区间(第三层循环就是枚举分的位置),两个区间的得分相加再加上这一次的得分,就是这个区间的总得分,取最大值和最小值。
代码如下:
#include<iostream> using namespace std; int n; int fax[210][210],fin[210][210]; int ansax[210][210],ansin[210][210]; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>fax[i][i]; fax[i+n][i+n]=fin[i+n][i+n]=fin[i][i]=fax[i][i]; } for(int k=1;k<=n-1;k++){ for(int i=1,j=i+k;i<=2*n,j<=2*n;i++,j++){ fin[i][j]=16000; ansin[i][j]=16000; for(int l=i;l<j;l++){ fax[i][j]=max(fax[i][j],fax[i][l]+fax[l+1][j]); fin[i][j]=min(fin[i][j],fin[i][l]+fin[l+1][j]); ansax[i][j]=max(ansax[i][j],fax[i][j]+ansax[i][l]+ansax[l+1][j]); ansin[i][j]=min(ansin[i][j],fin[i][j]+ansin[i][l]+ansin[l+1][j]); } } } int maxx=0; int minn=16000; for(int i=1;i<=n;i++){ maxx=max(maxx,ansax[i][i+n-1]); minn=min(minn,ansin[i][i+n-1]); } cout<<minn<<endl<<maxx; return 0; }
洛谷P1063 [NOIP2006 提高组] 能量项链
题目描述
在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m×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),为一个最优聚合顺序所释放的总能量。
输入输出样例
说明/提示
NOIP 2006 提高组 第一题
这道题就与上一题有一点不同,第三层循环还是枚举分的位置,但新区间的总能量是两个区间的能量的加和再加上这个区间的左右端点和分点的乘积。
代码如下:
#include<iostream> using namespace std; int n; int a[220]; int ans[220][220]; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; a[i+n]=a[i]; } for(int k=2;k<=n;k++){ for(int i=1,j=i+k;i<=2*n,j<=2*n;i++,j++){ for(int l=i+1;l<=j-1;l++){ ans[i][j]=max(ans[i][j],ans[i][l]+ans[l][j]+a[i]*a[l]*a[j]); } } } int maxx=0; for(int i=1;i<=n;i++){ maxx=max(maxx,ans[i][i+n]); } cout<<maxx; return 0; }
洛谷P4170 [CQOI2007]涂色
题目描述
假设你有一条长度为 5 的木板,初始时没有涂过任何颜色。你希望把它的 5 个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为 5 的字符串表示这个目标:RGBGR。
每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木板涂成RRRRR,第二次涂成 RGGGR,第三次涂成 RGBGR,达到目标。
用尽量少的涂色次数达到目标。
输入格式
输入仅一行,包含一个长度为 n 的字符串,即涂色目标。字符串中的每个字符都是一个大写字母,不同的字母代表不同颜色,相同的字母代表相同颜色。
输出格式
仅一行,包含一个数,即最少的涂色次数。
输入输出样例
说明/提示
40% 的数据满足 1≤n≤10。
100% 的数据满足 1≤n≤50。
这道题依旧是枚举分点,两区间相加,但如果两区间的端点颜色相同,那就可以在这个区间减少一次操作。
代码如下:
#include<iostream> #include<cstring> using namespace std; int n; char a[60]; int ans[100][100]; int main(){ scanf("%s",a); n=strlen(a); for(int i=1;i<=n;i++){ ans[i][i]=1; } for(int k=1;k<=n-1;k++){ for(int i=1,j=i+k;i<=n,j<=n;i++,j++){ ans[i][j]=n; for(int l=i;l<=j-1;l++){ if(a[i-1]==a[j-1]||a[i-1]==a[l+1-1]||a[j-1]==a[l-1]||a[l-1]==a[l+1-1]){ ans[i][j]=min(ans[i][j],ans[i][l]+ans[l+1][j]-1); } else{ ans[i][j]=min(ans[i][j],ans[i][l]+ans[l+1][j]); } } } } cout<<ans[1][n]; return 0; }
洛谷P1220 关路灯
题目描述
某一村庄在一条路线上安装了 n 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为 1m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位:m)、功率(W),老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
输入格式
第一行是两个数字 n(表示路灯的总数)和 c(老张所处位置的路灯号);
接下来 n 行,每行两个数据,表示第 1 盏到第 n 盏路灯的位置和功率。数据保证路灯位置单调递增。
输出格式
一个数据,即最少的功耗(单位:J,1J=1W×s)。
输入输出样例
说明/提示
样例解释
此时关灯顺序为 3 4 2 1 5
。
数据范围
1≤n≤50,1≤c≤n。
这道题不能直接一个个区间去计算,首先理清思路:老张从一个灯到一个灯中间经过的灯顺手就能关闭,也就是说老张站的位置一定是一个已经全部关闭的区间的一端,且除这个区间以外,其余的地方都是开着灯的,那么就需要将每一个区间存两个值,一个是存老张停在左端点时,另一个是存老张停在右端点时。然后状态转移时就是老张停在左端点时就是区间右部分停左端点+两位置之差乘开灯功率和区间右部分停右端点+两位置之差乘开灯功率的小值,停在右端点时就是区间左部分停左端点+两位置之差乘开灯功率和区间左部分停右端点+两位置之差乘开灯功率的小值,最后就比较整个区间停在右端点和停在左端点,取小值。
代码如下:
#include<iostream> #define Inf 0x3f3f3f using namespace std; int n,c; int a[60],zhi[60]; int f[60][60][4]; int shu[60][60]; int he; int main(){ cin>>n>>c; for(int i=1;i<=n;i++){ cin>>a[i]>>zhi[i]; he+=zhi[i]; } for(int k=0;k<=n-1;k++){ for(int i=1,j=i+k;i<=n,j<=n;i++,j++){ for(int p=i;p<=j;p++){ shu[i][j]+=zhi[p]; } } } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ f[i][j][1]=Inf; f[i][j][2]=Inf; } } f[c][c][1]=0; f[c][c][2]=0; for(int k=1;k<=n-1;k++){ for(int i=max(c-k,1),j=i+k;i<=c,j<=n;i++,j++){ for(int p=1;p<=2;p++){ f[i][j][1]=min(f[i+1][j][1]+(he-shu[i+1][j])*(a[i+1]-a[i]),f[i+1][j][2]+(he-shu[i+1][j])*(a[j]-a[i])); f[i][j][2]=min(f[i][j-1][1]+(he-shu[i][j-1])*(a[j]-a[i]),f[i][j-1][2]+(he-shu[i][j-1])*(a[j]-a[j-1])); } } } cout<<min(f[1][n][1],f[1][n][2]); return 0; }
洛谷P1043 [NOIP2003 普及组] 数字游戏
题目描述
丁丁最近沉迷于一个数字游戏之中。这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。游戏是这样的,在你面前有一圈整数(一共 nn 个),你要按顺序将其分为 mm 个部分,各部分内的数字相加,相加所得的 mm 个结果对 1010 取模后再相乘,最终得到一个数 kk。游戏的要求是使你所得的 kk 最大或者最小。
例如,对于下面这圈数字(n=4n=4,m=2m=2):
要求最小值时,((2-1)\bmod10)\times ((4+3)\bmod10)=1\times 7=7((2−1)mod10)×((4+3)mod10)=1×7=7,要求最大值时,为 ((2+4+3)\bmod10)\times (-1\bmod10)=9\times 9=81((2+4+3)mod10)×(−1mod10)=9×9=81。特别值得注意的是,无论是负数还是正数,对 1010 取模的结果均为非负值。
丁丁请你编写程序帮他赢得这个游戏。
输入格式
输入文件第一行有两个整数,nn (1\le n\le 501≤n≤50) 和 mm (1\le m\le 91≤m≤9)。以下 nn 行每行有个整数,其绝对值 \le10^4≤104,按顺序给出圈中的数字,首尾相接。
输出格式
输出文件有 22 行,各包含 11 个非负整数。第 11 行是你程序得到的最小值,第 22 行是最大值。
输入输出样例
说明/提示
【题目来源】
NOIP 2003 普及组第二题
这道题也是首尾相接的,但要多一维(切的数量),就是在这个区间中切了几刀。
代码如下:
#include<iostream> using namespace std; int n,m; int a[110]; int fax[110][110][20],fin[110][110][20]; int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; a[i]+=a[i-1]; a[i]%=10; a[i]+=10; a[i]%=10; } for(int i=1;i<=n;i++){ a[i+n]=(a[i]+a[n])%10; } for(int i=1;i<=2*n;i++){ for(int j=1;j<=2*n;j++){ fin[i][j][0]=(a[j]-a[i-1]+10)%10; fax[i][j][0]=(a[j]-a[i-1]+10)%10; } } for(int k=1;k<=m-1;k++){ for(int p=1;p<=n-1;p++){ for(int i=1,j=i+p;i<=2*n,j<=2*n;i++,j++){ fin[i][j][k]=1000000000; for(int l=i;l<=j-1;l++){ for(int h=max(0,k-1-(j-(l+1)));h<=k-1&&h<=l-i;h++){ fin[i][j][k]=min(fin[i][j][k],fin[i][l][h]*fin[l+1][j][k-1-h]); fax[i][j][k]=max(fax[i][j][k],fax[i][l][h]*fax[l+1][j][k-1-h]); } } } } } int maxn=0; int minn=1000000000; for(int i=1;i<=n;i++){ maxn=max(maxn,fax[i][i+n-1][m-1]); minn=min(minn,fin[i][i+n-1][m-1]); } cout<<minn<<endl<<maxn; return 0; }