本质
区间dp的本质其实是线性dp的一种。
线性dp:从初始状态开始,沿着阶段的扩张向某个方向递推,直至计算出目标状态。
区间dp中是以 “区间长度” 作为dp的阶段。
区间dp的初始状态一般是长度为1的区间。
NOIP 2006 提高组
下面以例题(石子合并)分析:
简单:石子合并
题目描述:
设有N堆石子排成一排,其编号为1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为:4+9+11=24;
如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为:4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数N表示石子的堆数N。
第二行N个数,表示每堆石子的质量(均不超过1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1 ≤ N ≤ 300 1≤N≤300 1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
以最后一次分界线的位置(k表示最后一次分界线的位置)来分类。
可以表示为:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<map>
#include<algorithm>
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define ll long long
#define int ll
#define inf 0x3f3f3f3f
using namespace std;
int read()
{
int w = 1, s = 0;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-') w = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';ch = getchar(); }
return s * w;
}
//最大公约数
int gcd(int x,int y) {
if(x<y) swap(x,y);//很多人会遗忘,大数在前小数在后
//递归终止条件千万不要漏了,辗转相除法
return x % y ? gcd(y, x % y) : y;
}
//计算x和y的最小公倍数
int lcm(int x,int y) {
return x * y / gcd(x, y);//使用公式
}
int ksm(int a, int b, int mod) {
int s = 1; while(b) {
if(b&1) s=s*a%mod;a=a*a%mod;b>>=1;}return s;}
const int N = 310;
int n;
int sum[N];
int dp[N][N];
signed main()
{
int n = read();
for (int i = 1;i <= n; ++i) {
sum[i] = read();
}
//用个前缀和来存结果。
for (int i = 1; i <= n; ++i) {
sum[i] += sum[i - 1];
}
//枚举 区间长度 即dp中常说的阶段
for (int len = 2; len <= n; ++len) {
//枚举左端点
for (int l = 1; l + len - 1 <= n; ++l) {
int r = l + len - 1;//右端点
dp[l][r] = inf;
//枚举分界线
for (int k = l; k < r; ++k) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + sum[r] - sum[l - 1]);
}
}
}
printf("%lld\n", dp[1][n]);
return 0;
}
简单:洛谷 P1880 [NOI1995] 环形石子合并
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
输入格式
第一行包含整数 n,表示共有 n 堆石子。
第二行包含 n 个整数,分别表示每堆石子的数量。
输出格式
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
数据范围
1 ≤ n ≤ 200 1≤n≤200 1≤n≤200
输入样例:
4
4 5 9 4
输出样例:
43
54
其他大致与 石子合并 差不多:注意本题要求一个最大值一个最小值:dpmax[N][N],dpmin[N][N];(其余详情请看代码,代码有详细注释!!)
/*
区间DP一般有两种代码实现方式:
迭代式 (推荐)
for(int len = 1;i len <= n; len ++) {
//左端点
for (int L = 1; L + len - 1 <= n; L++) {
R = L + len - 1;//右端点
}
}
*/
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int read()
{
int w = 1, s = 0;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-') w = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';ch = getchar(); }
return s * w;
}
const int N = 410;//因为要开双链
const int inf = 0x3f3f3f3f;
int sum[N], val[N];//sum[N]表示合并付出的代价总和,val[N]表示每个点的值。
int dpmin[N][N], dpmax[N][N];//得分总和最小,得分总和最大
int main()
{
int n = read();
for (int i = 1; i <= n; ++i) {
val[i] = read();
val[i + n] = val[i];//化环形为链形
}
for (int i = 1; i <= n + n; ++i) {
sum[i] = sum[i - 1] + val[i];//前缀和 以便最后求出——最后一步所需要付出的代价
}
//初始化
memset(dpmin, 0x3f, sizeof dpmin);
memset(dpmax, -0x3f, sizeof dpmax);
//枚举链的长度
for (int len = 1; len <= n; ++len) {
//枚举区间左端点
for (int l = 1; l + len - 1 <= n + n; ++l) {
int r = l + len - 1;//区间右端点
//如果区间长度为1,则无需付出任何代价。
if (len == 1) dpmin[l][r] = dpmax[l][r] = 0;
else {
//枚举分界线
for (int k = l; k < r; ++k) {
dpmin[l][r] = min(dpmin[l][r], dpmin[l][k] + dpmin[k + 1][r] + sum[r] - sum[l - 1]);
dpmax[l][r] = max(dpmax[l][r], dpmax[l][k] + dpmax[k + 1][r] + sum[r] - sum[l - 1]);
}
}
}
}
//初始化最大值最小值。
int maxv = -inf, minv = inf;
for (int l = 1; l <= n; ++l) {
maxv = max(maxv, dpmax[l][l + n - 1]);
minv = min(minv, dpmin[l][l + n - 1]);
}
//得出结果
printf("%d\n", minv);
printf("%d\n", maxv);
return 0;
}
简单:能量项链
在Mars星球上,每个Mars人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。
能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。
并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。
因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。
如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 mrn(Mars单位),新产生的珠子的头标记为 m,尾标记为 n。
需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。
显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。
我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第 j,k 两颗珠子聚合后所释放的能量。则
第4、1两颗珠子聚合后释放的能量为:(4⊕1)=1023=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为((4⊕1)⊕2)⊕3)= 10 × 2 × 3+10 × 3 × 5+10 × 5 × 10=710。
输入格式
输入的第一行是一个正整数 N,表示项链上珠子的个数。
第二行是N个用空格隔开的正整数,所有的数均不超过1000,第 i 个数为第 i 颗珠子的头标记,当i<N时,第 i 颗珠子的尾标记应该等于第 i+1 颗珠子的头标记,第 N 颗珠子的尾标记应该等于第1颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
输出格式
输出只有一行,是一个正整数 E,为一个最优聚合顺序所释放的总能量。
数据范围
4 ≤ N ≤ 100 , 4≤N≤100, 4≤N≤100,
1 ≤ E ≤ 2.1 ∗ 1 0 9 1≤E≤2.1∗10^9 1≤E≤2.1∗109
输入样例:
4
2 3 5 10
输出样例:
710
题目大意:
火星人带能量项链,其中有N颗能量珠,珠子有前标记和后标记,如果前一颗珠子的前标记 == 后一颗珠子的前珠子标记,那么这两颗珠子就可以合并成一颗珠子,能量值就是:w[l] * w[k] * w[r](解释在后面)。
这道题可以将珠子看成矩阵。
珠子前标记看成矩阵的行,后标记看成矩阵的列。
以样例为例子:
如果是链式:
样例链式分布:
那么跟上一题一样:我们可以通过化环为链的思想,用2n的链模拟环形的结果。
(这种“任意选择一个位置断开,复制形成两倍长度的链”的方法,是解决DP的环形结构的常用手段之一。)
最后一步:一定是将三个长度的区间合并成两个长度的区间。
上代码:
#include<cstdio>
#include<iostream>
using namespace std;
const int N = 210;//将n的环化为2n的链
int n;
int w[N];
int f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> w[i];
w[i + n] = w[i];//依次copy前面的
}
//枚举区间长度,区间最短为3,因为最终是合成长度为2的区间。
for (int len = 3; len <= n + 1; ++len) {
//枚举左端点
for (int l = 1; l + len - 1 <= n + n; ++l) {
int r = l + len - 1;//右端点
//枚举分界线
for (int k = l + 1; k < r; ++k) {
f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
}
}
}
int ans = 0;
for (int l = 1; l <= n; l++) {
ans = max(ans, f[l][l + n]);
}
cout << ans << endl;
return 0;
}
kuangbin LightOJ - 1422
题意:
小灰灰参加圣诞节的一些派对,并且需要穿上对应派对的衣服,所以他需要多次换衣服,为了方便,他可以选择脱掉一些衣服或者穿上新衣服,比如说,他穿着超人的衣服,外面又穿着死侍的衣服,当他要参加超人服装派对时,他可以选择脱掉死侍的衣服(因为死侍衣服的里面有超人的衣服),或者他可以在穿一件超人的衣服,小灰灰是个爱干净的人,当他脱下死侍的衣服后,如果需要再穿死侍的衣服,他会选择再穿一件新的。(如果他先穿A衣服,又穿上B衣服,再穿一件C衣服,如果他想让最外面的衣服是A,他可以选择直接穿一件A,或者先把C脱掉,再把B脱掉)。
输入
第一行输入一个T,表示测试案例的数量 T<=200 N和a[i]<=100
接下来一行输入一个N,表示派对个数,
接下来一行n个数,表示第i个派对他会穿着a[i]的衣服参加这场派对(派对的前后顺序不可调换)
输出
对于每个测试案例,输出“Case i: ”加所需服装的最小数量。
案例输入
2
4
1 2 1 2
7
1 2 1 1 3 2 1
案例输出
Case 1: 3
Case 2: 4
分析:待补
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<map>
#include<algorithm>
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define ll long long
#define int ll
#define INF 0x3f3f3f3f
#define PI acos(-1)
#define MOD 1e9 + 7
using namespace std;
int read()
{
int w = 1, s = 0;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-') w = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';ch = getchar(); }
return s * w;
}
//最大公约数
int gcd(int x,int y) {
if(x<y) swap(x,y);//很多人会遗忘,大数在前小数在后
//递归终止条件千万不要漏了,辗转相除法
return x % y ? gcd(y, x % y) : y;
}
//计算x和y的最小公倍数
int lcm(int x,int y) {
return x * y / gcd(x, y);//使用公式
}
int ksm(int a, int b, int mod) {
int s = 1; while(b) {
if(b&1) s=s*a%mod;a=a*a%mod;b>>=1;}return s;}
//------------------------ 以上是我常用模板与刷题几乎无关 ------------------------//
const int N = 110;
int dp[N][N], num[N];
signed main()
{
int t = read();
int kase = 0;
while (t--) {
int n = read();
for (int i = 1; i <= n; ++i) num[i] = read();
memset(dp, 0, sizeof dp);
//枚举长度
for (int len = 1; len <= n; ++len) {
//枚举左端点
for (int l = 1; l + len - 1 <= n; ++l) {
int r = l + len - 1;
dp[l][r] = dp[l + 1][r] + 1;
//枚举分界线
for (int k = l + 1; k <= r; ++k) {
if (num[l] == num[k]) dp[l][r] = min(dp[l][r], dp[l + 1][k] + dp[k + 1][r]);
}
}
}
printf("Case %d: %d\n", ++kase, dp[1][n]);
}
return 0;
}
kuangbin POJ - 2955
题目描述
用以下方式定义合法的括号字符串
1.空串是合法的
2. 如果S是合法的, 那么(S)和[S]也都是合法的
3. 如果A和B是合法的, 那么AB是一个合法的字符串.
举个栗子, 下列字符串都是合法的括号字符串:
(), [], (()), ([]), ()[], ()[()]
下面这些不是:
(, [, ), )(, ([)], ([(]
给出一个由字符’(’, ‘)’, ‘[’, 和’]‘构成的字符串. 你的任务是找出一个最长的合法字符串的长度,使这个的字符串是给出的字符串的子序列。对于字符串a1 a2 … an, b1 b2 … bm 当且仅当对于1 = i1 < i2 < … < in = m, 使得对于所有1 = j = n,aj = bij时, aj是bi的子序列
输入
多组数据. 每组数据在一行上输入一个只含有’(’, ‘)’, ‘[’, ']'字符的字符串,字符串的最大长度是100, 输入字符串"end"结束
输出
对于每组数据, 在单独的一行上输出题目描述中所求的长度
样例输入
((()))
()()()
([]])
)[)(
([][][)
end
样例输出
6
6
4
0
6
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<map>
#include<algorithm>
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define ll long long
#define int ll
#define INF 0x3f3f3f3f
#define PI acos(-1)
#define MOD 1e9 + 7
using namespace std;
int read()
{
int w = 1, s = 0;
char ch = getchar();
while (ch < '0' || ch>'9') {
if (ch == '-') w = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';ch = getchar(); }
return s * w;
}
//最大公约数
int gcd(int x,int y) {
if(x<y) swap(x,y);//很多人会遗忘,大数在前小数在后
//递归终止条件千万不要漏了,辗转相除法
return x % y ? gcd(y, x % y) : y;
}
//计算x和y的最小公倍数
int lcm(int x,int y) {
return x * y / gcd(x, y);//使用公式
}
int ksm(int a, int b, int mod) {
int s = 1; while(b) {
if(b&1) s=s*a%mod;a=a*a%mod;b>>=1;}return s;}
//------------------------ 以上是我常用模板与刷题几乎无关 ------------------------//
const int N = 110;
int dp[N][N], num[N];
bool check(char ch1, char ch2) {
if (ch1 == '(' && ch2 == ')') return true;
if (ch1 == '[' && ch2 == ']') return true;
return false;
}
signed main()
{
while(true) {
string s;
cin >> s;
if (s == "end") break;
memset(dp, 0, sizeof dp);
int n = s.size();
//枚举长度
for (int len = 1; len < n; ++len) {
//枚举左端点
for (int l = 0; l + len < n; ++l) {
int r = l + len;
if (check(s[l], s[r])) dp[l][r] = dp[l + 1][r - 1] + 2;
//枚举分界线
for (int k = l; k < r; ++k) dp[l][r] = max(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
printf("%lld\n", dp[0][n - 1]);
}
return 0;
}