动态规划--区间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<iostream>
#include <algorithm>
#include <cstdio>
#include<cmath>
#include <cstring>
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)。
两只兔子,在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公式仅仅是求出一个序列的非连续最长回文子序列,题目的序列是环状的,有两种思路:
将环倍增成链,求出窗口为n的最长子序列,但这不是最终的解,你可以试看看Sample 2,是只能得出4,因为它在选中的回文外面还可以选中一个当做起点来跳,所以外面得判断找出来的回文外面是否还有可以当起点的石头,即可以找窗口为(n-1)的长度+1。所以解即找 窗口为n的长度或者 窗口为(n-1)的长度+1 的最大值。
不倍增,直接当成一个链求dp,然后把链切成两半,求出两边的回文长度,最大的和就是解。这里不用考虑起点问题,因为两边的回文中点都可以做起点。因为它是两边一起跑 也就是可以是两段回文子序 所以。。只需要求下1-i i+1-n的最长回文串就可以了 这个是可以在之前求总的时候保留下来的
解法一:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
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<iostream>
#include <algorithm>
#include <cstdio>
#include<cmath>
#include <cstring>
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;
}
题目大意:
一些屌丝排队进场,第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<iostream>
#include <algorithm>
#include <cstdio>
#include<cmath>
#include <cstring>
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
End