动态规划--区间DP

动态规划--区间DP


所谓区间dp,顾名思义就是在一段区间上的动态规划。它既要满足dp问题的最优子结构和无后效性外,还应该符合在区间上操作的特点。我的理解是往往会对区间进行合并操作。亦或是单个元素(可看成一个小区间)跨区间进行操作。例如括号匹配问题,石子合并问题(通过多次的相邻合并,最后实质上会产生跨区间的合并,如果你把其中的石子看作参考系的话就很容易感觉出来),还有在整数中插入运算符号的问题(利用运算符的优先级以及交换律可看出)

区间dp,一般是枚举区间,把区间分成左右两部分,然后求出左右区间再合并。这样以来,如果我们要得知一个大区间的情况,由于它必定是由从多个长度不一的小区间转移而来(转移情况未知),我们可以通过求得多个小区间的情况,从而合并信息,得到大区间。


对于一个长度为n的区间,确定它的子区间需要首尾两个指针,显然子区间数量级为n^2,那区间dp的复杂度也就为n^2。

********************************************************************************************************************

 1.    poj 1141 Brackets Sequence     括号匹配并输出方案

 2.     hdu 4745 Two Rabbits     转化成求回文串 
 3.     hdu 4283 You Are the One      常见写法
*************************************************************************************************************

1.    poj 1141 Brackets Sequence     括号匹配并输出方案

题意:给一个由[,],{,}组成的字符串序列,求增加最少的字符,使该序列能够匹配,并输出最后的方案。

解题思路:

区间dp.dp[i][j]表示从i~j 所需的最少的字符使之能匹配,转移的话要么是头尾匹配直接加中间,要么分成两段。
不过要输出到达路径,所以在用一个path[i][j]表示到达该路径时的选择,-1表示头尾,其他表示中间分开的位置。
递归输出路径。递归是个好东西,能够很大程度的改变顺序,特别是逆着的。
#define _CRT_SECURE_NO_WARNINGS
#include
#include 
#include 
#include
#include 
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 110
int dp[maxn][maxn];     //dp[i][j]表示i->j之间最少插入字符数使之匹配
int path[maxn][maxn];   //path[i][j]表示到达该路径时的选择,-1表示头尾,其他表示中间分开的位置。
char str[maxn];
int len;

void output(int l, int r)
{
	if (l > r) return;

	if (l == r)  //最后一个位置
	{
		if (str[l] == '(' || str[l] == ')') cout << "()";
		else cout << "[]";
		return;
	}

	if (path[l][r] == -1)  //相等位置,输出:左-递归中间-右
	{
		cout << str[l];
		output(l + 1, r - 1);
		cout << str[r];
	}

	else
	{
		output(l, path[l][r]);
		output(path[l][r] + 1, r);
	}
}

int main(int argc, const char * argv[]) 
{
	while (gets(str) != NULL)   //注意scanf不能读空串
	{
		len = strlen(str);  
		memset(dp, 0, sizeof(dp));
		for (int i = 0; i < len; i++) dp[i][i] = 1; // 一个就再对应匹配一个

		for (int l = 1; l < len; l++)   //区间长度
		{
			for(int i = 0, j = l; j < len; i++, j++) //i开始,j结束
			{
				dp[i][j] = inf;
				if (((str[i] == '['&&str[j] == ']') || (str[i] == '('&&str[j] == ')')) &&dp[i][j]>dp[i+1][j-1])  //后面的>必定成立
				{
					dp[i][j] = dp[i + 1][j - 1];
					path[i][j] = -1;
				}
				for (int pos = i; pos < j; pos++)  //中间位置
				{
					if (dp[i][j] > dp[i][pos] + dp[pos + 1][j])
					{
						dp[i][j] = dp[i][pos] + dp[pos + 1][j];
						path[i][j] = pos;
					}
				}
			}
		}
		output(0, len - 1);
		cout << endl;
	}
	
	return 0;
}

2.    hdu 4745 Two Rabbits     转化成求回文串

题意:
两只兔子,在n块围成一个环形的石头上跳跃,每块石头有一个权值ai,一只从左往右跳,一只从右往左跳,每跳一次,两只兔子所在的石头的权值都要相等,在一圈内(各自不能超过各自的起点,也不能再次回到起点)它们最多能经过多少个石头(1 <= n <= 1000, 1 <= ai <= 1000)。

分析:

其实就是求一个环中,非连续最长回文子序列的长度。
dp[i][j] = max{ dp[i + 1][j], d[i][j - 1], (if a[i] == a[j]) dp[i + 1][j - 1] + 2 }
但是,这个dp公式仅仅是求出一个序列的非连续最长回文子序列,题目的序列是环状的,有两种思路:

  1. 将环倍增成链,求出窗口为n的最长子序列,但这不是最终的解,你可以试看看Sample 2,是只能得出4,因为它在选中的回文外面还可以选中一个当做起点来跳,所以外面得判断找出来的回文外面是否还有可以当起点的石头,即可以找窗口为(n-1)的长度+1。所以解即找 窗口为n的长度或者 窗口为(n-1)的长度+1 的最大值。

  2. 不倍增,直接当成一个链求dp,然后把链切成两半,求出两边的回文长度,最大的和就是解。这里不用考虑起点问题,因为两边的回文中点都可以做起点。因为它是两边一起跑 也就是可以是两段回文子序 所以。。只需要求下1-i i+1-n的最长回文串就可以了 这个是可以在之前求总的时候保留下来的

解法一:
#include 
#include 
#include 
#include 
using namespace std;

const int N = 1001<<1;

int dp[N][N];
int a[N];
int n;

int main() {
	while (scanf("%d", &n) && n) {
		for (int i = 1; i <= n; i++) {
			scanf("%d", &a[i]);
			a[n + i] = a[i];
		}

		memset(dp, 0, sizeof(dp));
		for (int i = 1; i <= 2 * n; i++)
			dp[i][i] = 1;

		for (int len = 1; len < 2 * n; len++) {
			for (int i = 1; i + len <= 2 * n; i++) {
				int j = i + len;
				dp[i][j] = max(dp[i + 1][j], max(dp[i][j - 1], (a[i] == a[j] ? dp[i + 1][j - 1] + 2 : 0)));
			}
		}

		int ans = 0;
		for (int i = 1; i <= n; i++)
			ans = max(ans, dp[i][i + n - 1]);
		for (int i = 1; i <= n; i++)
			ans = max(ans, dp[i][i + n - 2] + 1);
		printf("%d\n", ans);
	}
	return 0;
}

解法二:
#define _CRT_SECURE_NO_WARNINGS
#include
#include 
#include 
#include
#include 
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 1100
int dp[maxn][maxn];
int a[maxn], n;

int main() 
{
	while (cin >> n&&n)
	{
		memset(dp, 0, sizeof(dp));
		for (int i = 0; i < n; i++)
		{
			cin >> a[i];
			dp[i][i] = 1;
		}

		for (int l = 1; l < n; l++)  //区间长度
		{
			for (int i = 0, j = l; j < n; i++, j++)  //区间 i->j之间的回文
			{
				dp[i][j] = max(dp[i][j], max(dp[i + 1][j], max(dp[i][j - 1], (a[i] == a[j]) ? dp[i + 1][j - 1] + 2 : 0)));
			}
		}

		int ans = 0;
		for (int i = 0; i < n; i++)
			ans = max(ans, dp[0][i] + dp[i + 1][n - 1]);
		cout << ans << endl;
	}

	return 0;
}
 
3.   hdu 4283 You Are the One      

题目大意:

一些屌丝排队进场,第k个进场的人后又k-1*a[i]的愤怒值,为了得到最小的愤怒值,可以利用一个栈来调整顺序,第i个人进栈可以让第i+1个人先行入场,对于栈里的元素必须是后进先出,问如何合理利用栈来以得到最小的愤怒值
解题思路:
我们用dp[i][j]表示区间i~j之中的元素可得到的最小的愤怒值。对于i~j中的元素i我们然他第k个入场,那么其后面的k-1个元素就要先行入场,这是问题就变成了dp[i+1][i+k-1]和dp[i+k][j], 对于第i个元素的愤怒值为:(k-1)*a[i],而第i+k~j的愤怒值要加上k*(sum[j]-sum[i+1-1]);  最后dp[0][n-1]就是答案
#define _CRT_SECURE_NO_WARNINGS
#include
#include 
#include 
#include
#include 
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 110
int dp[maxn][maxn];
int a[maxn], n;
int sum[maxn];

int main() 
{
	int t, cas = 1;
	cin >> t;
	while (t--)
	{
		cin >> n;
		memset(dp, 0, sizeof(dp));
		memset(sum, 0, sizeof(sum));
		for (int i = 0; i < n; i++)
		{
			cin >> a[i];
			sum[i] = sum[i - 1] + a[i];
		}
		for (int i = 0; i < n; i++)
		{
			for (int j = i + 1; j < n; j++)
			{
				dp[i][j] = inf;
			}
		}

		for (int l = 1; l < n; l++)  //区间
		{
			for (int i = 0, j = l; j < n; i++, j++)
			{
				for (int k = 1; k <= j - i + 1; k++)
				{
					dp[i][j] = min(dp[i][j], dp[i + 1][i + k - 1] + (k - 1) * a[i] + dp[i + k][j] + k*(sum[j] - sum[i + k - 1]));
				}

			}
		}
		printf("Case #%d: %d\n", cas++, dp[0][n-1]);
	}
	return 0;
}

推荐:

zoj 3541 The Last Puzzle  贪心+区间dp

poj 2955 Brackets

hdu 2476 String Printer 

zoj 3537 Cake

CF 149D Coloring Brackets

zoj 3469 Food Delivery

End

你可能感兴趣的:(算法知识,dp,动态规划学习)