区间DP原理解析 和 kuangbin一些题

本质

区间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 1N300

输入样例:

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 1n200
输入样例:

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, 4N100,
1 ≤ E ≤ 2.1 ∗ 1 0 9 1≤E≤2.1∗10^9 1E2.1109

输入样例:

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;
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值