区间DP是线性DP的扩展,分阶段地划分问题,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
状态转移方程:
区间DP的特点:
(1)合并:即将两个或多个部分进行整合,当然也可以反过来。
(2)特征:能将问题分解为能两两合并的形式。
(3)求解:将整个问题舍最优值,枚举合并点,将问题分解为左右两个部分,最后合并的两个部分的最优值得到原问题的最优值。
简而言之:通过合并小区间的最优解进而得出整个大区间上的最优解。
将区间分割成一个个小区间,求解每个小区间上的最优解。
代码实现,可以枚举分割长度,起点和分割点,更新小区间的最优解。
1.枚举区间长度 2.枚举起点也得到了重点。3.枚举分割点即可。
for(int len = 1; len <= n; len++) // 枚举长度
{
for(int i = 1; i + len <= n + 1; i++) // 枚举起点
{
int j = i + len - 1;
for(int k = i; k < j; k++) // 枚举分割点 注意没有等于号
{
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
xp[i][j] = max(xp[i][j], xp[i][k] + xp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
简单的区间DP模板题,区间[1, n]上的代价sum[n] = sum[n - 1] + a[n]
#include
using namespace std;
const int maxn = 110;
int n, a[maxn], dp[maxn][maxn], sum[maxn], xp[maxn][maxn];
int main()
{
while(scanf("%d", &n) != EOF)
{
memset(xp, 0, sizeof(xp));
memset(sum, 0, sizeof(sum));
memset(dp, 0x3f3f3f3f, sizeof(dp));
for(int i = 1; i <= n; i++)
{
dp[i][i] = 0;
xp[i][i] = 0;
scanf("%d", &a[i]);
sum[i] = sum[i - 1] + a[i];
}
for(int len = 1; len <= n; len++)
{
for(int i = 1; i + len <= n + 1; i++)
{
int j = i + len - 1;
for(int k = i; k < j; k++)
{
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
xp[i][j] = max(xp[i][j], xp[i][k] + xp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
printf("%d %d\n", dp[1][n], xp[1][n]);
}
return 0;
}
相比较于直线版的石子合并我们可以在后面加上一个相同大小的区间。可以模拟到圆环的状态。
#include
#include
#include
using namespace std;
const int maxn = 210;
int n, dp[maxn][maxn], xp[maxn][maxn], sum[maxn], a[maxn];
int main()
{
while(scanf("%d", &n) != EOF)
{
memset(dp, 0x3f, sizeof(dp));
memset(xp, 0, sizeof(xp));
memset(sum, 0, sizeof(sum));
for(int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
sum[i] = sum[i - 1] + a[i];
dp[i][i] = 0;
}
for(int i = 1; i <= n; i++)
{
sum[i + n] = sum[i + n - 1] + a[i];
dp[i + n][i + n] = 0;
}
for(int len = 1; len <= n;len++)
{
for(int i = 1; i + len <= 2 * n + 1; i++)
{
int j = len + i - 1;
for(int k = i; k < j; k++)
{
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
xp[i][j] = max(xp[i][j], xp[i][k] + xp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
int maxns = -1, minns = 0x3f3f3f3f;
for(int i = n; i <= 2 * n; i++)
{
maxns = max(xp[i - n + 1][i], maxns);
minns = min(dp[i - n + 1][i], minns);
}
printf("%d %d\n", minns, maxns);
}
return 0;
}
思路:在查找最优分割点的时候我们浪费了大量的时间,我们可以保存最有分割点来优化查找过程。
四边形不等式优化:
(1)功能:用来寻找,s[i][j](i~j的最优分割点)与其他分割点的关系
(2)不等式内容:如果某东西满足a 证明过程请参考这位累加器大佬的博客嘻嘻
然后发一波模板
for(int len = 1; len <= n; len++)
{
for(int i = 1; i + len <= 2 * n + 1; i++)
{
int j = i + len - 1;
for(int k = re[i][j - 1]; k <= re[i + 1][j]; k++)
{
if(dp[i][j] > dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1])
{
dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
re[i][j] = k;
}
}
}
}
这里呢他是给了你40000一个略大的数据如果再易找朴素区间DP或者它的平行四边形优化的话可能就降不住了,在这里给大家介绍一种简单的一维开出n方复杂度的算法。GarsiaWachs算法。
设序列是stone[],从左往右,找一个满足stone[k-1] <= stone[k+1]的k,找到后合并stone[k]和stone[k-1],再从当前位置开始向左找最大的j,使其满足stone[j] > stone[k]+stone[k-1],插到j的后面就行。一直重复,直到只剩下一堆石子就可以了。在这个过程中,可以假设stone[-1]和stone[n]是正无穷的。
举个例子:
186 64 35 32 103
因为35<103,所以最小的k是3,我们先把35和32删除,得到他们的和67,并向前寻找一个第一个超过67的数,把67插入到他后面,得到:186 67 64 103,现在由5个数变为4个数了,继续:186 131 103,现在k=2(别忘了,设A[-1]和A[n]等于正无穷大)234 186,最后得到420。最后的答案呢?就是各次合并的重量之和,即420+234+131+67=852。
基本思想是通过树的最优性得到一个节点间深度的约束,之后证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的深度不会改变。具体实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化,使得最终复杂度为O(nlogn)。
模板实现:
#include
#include
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 40005;
int a[maxn], n, ans, t;
void combine(int k)
{
int temp = a[k - 1] + a[k];
ans += temp;
--t;
for(int i = k; i < t; ++i) a[i] = a[i + 1];
int j;
for(j = k - 1; a[j - 1] < temp; --j) a[j] = a[j - 1];
a[j] = temp;
while(j >= 2 && a[j] >= a[j - 2])
{
int d = t - j;
combine(j - 1);
j = t - d;
// cout<<"--------"< 3) combine(t - 1);
printf("%d\n", ans);
return 0;
}
1.题意:只有两种括号( )和[ ], 要求最大的括号匹配数。
2.如果s[i] 与s[j]匹配, 则dp[i][j] = dp[i + 1][j-1] + 2; 并且我们需要找的是最大的匹配数目,所以不断更新区间情况,来使达到最大最优匹配状态。
3.状态转移方程
if(s[i] 与 s[j]匹配) dp[i][j] = d[[i+1][j-1] +2;
dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]);
#include
#include
#include
using namespace std;
const int maxn = 110;
bool match(char a, char b)
{
if(a == '(' && b == ')' || a == '[' && b == ']')
return 1;
return 0;
}
char ch[maxn];
int dp[maxn][maxn];
int main()
{
while(scanf("%s", ch + 1) != EOF)
{
if(ch[1] == 'e') break;
int n = strlen(ch + 1);
memset(dp, 0, sizeof(dp));
for(int len = 1; len <= n; len++)
{
for(int i = 1; i + len <= n + 1; i++)
{
int j = i + len - 1;
if(match(ch[i], ch[j])) dp[i][j] = dp[i + 1][j - 1] + 2; // 匹配
for(int k = i; k < j; k++)
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
printf("%d\n", dp[1][n]);
}
return 0;
}
抽卡片,然后获得的分数是抽到的卡片* 左边 * 右边,但不能删两端两张卡片,让我们求得是获得分数的最小值。
因为要删掉中间得所有数字,所以我们可以把序列化成一个小区间不断地删, 合并区间来求最小。
dp[i][j]表示抽出第i~j-1张卡片时候的最小值
dp[i][j] = min(dp[i][j],dp[i][k] + dp[k+1][j] +num[i-1]*num[k]*num[j]);
#include
#include
#include
using namespace std;
const int maxn = 110;
int n, a[maxn], dp[maxn][maxn];
int main()
{
scanf("%d", &n);
memset(dp, 0x3f, sizeof(dp));
for(int i = 1; i <= n; i++)
{
dp[i][i] = 0;
scanf("%d", &a[i]);
}
for(int len = 1; len <= n; len++)
{
for(int i = 2; i + len <= n + 1; i++) // 因为不能去两端所以起点为2
{
int j = len + i - 1;
for(int k = 1; k < j; k++)
{
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i - 1] * a[k] * a[j]);
}
}
}
printf("%d\n", dp[2][n]);
return 0;
}
问题是我们经常见到的整数划分,给出两个整数 n , m ,要求在 n 中加入m - 1 个乘号,将n分成m段,求出这m段的最大乘积
第一行是一个整数T,表示有T组测试数据 接下来T行,每行有两个正整数 n,m ( 1<= n < 10^19, 0 < m <= n的位数);
输出每组测试样例结果为一个整数占一行
2
111 2
1111 2
11 121
题意
给你一个整数,让你用m个乘号进行划分,让你求最大的值。
思路
因为乘号为有限个,我们要记录乘号的位置和插入它的位置,dp[i][j]用来表示1 - i,插入j个乘号的值。
用num[i][j]来表示i--j的数。
状态转移方程
状态转移方程 dp[i][j]表示在第1~i个字符里插入j个乘号的最大值;用num[i][j]表示第i~j个字符表示的数字;
dp[i][j] = max(dp[i][j],dp[k][j-1]*num[k+1][i]) // 表示在[1,k]和[k + 1][i]k这个位置插入一个乘号。
#include
using namespace std;
const int maxn = 30;
int T, m;
long long num[maxn][maxn], dp[maxn][maxn];
char ch[maxn];
int main()
{
scanf("%d", &T);
while(T--)
{
scanf("%s", ch + 1);
scanf("%d", &m);
int n = strlen(ch + 1);
memset(dp, 0, sizeof(dp));
memset(num, 0, sizeof(num));
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
for(int k = i; k <= j; k++)
{
num[i][j] *= 10;
num[i][j] += (ch[k] - '0');
}
}
dp[i][0] = num[1][i];
}
for(int j = 1; j < m; j++)
{
for(int i = 1; i <= n; i++)
{
for(int k = 1; k < i; k++)
{
dp[i][j] = max(dp[i][j], dp[k][j - 1] * num[k + 1][i]); // 在k处插入乘号
}
}
}
printf("%lld\n", dp[n][m - 1]);
}
return 0;
}
题意:
给定一个字符串,让你求它回文子字符串的个数,(这些字符可以不必连续),这里不同的回文字串只要求位置不同即可视为不同
分析:
用dp[i][j]表示状态,表示i~j里最多的回文字串数目,假设现在我们要求dp[i][j]:
a.首先:由前一个状态知:dp[i][j] = dp[i+1][j]并上dp[i][j-1] (因为区间尽可能大而且状态要在dp[i][j]之前,而且回文子串不要求 连续),由容斥原理得:dp[i+1][j] U dp[i][j-1] = dp[i+1][j]+dp[i][j-1] - dp[i+1][j] n dp[i][j-1] = dp[i+1][j]+dp[i][j-1] - dp[i+1][j-1]
注意:这是一个固定的状态,每一个状态都由这个公式推出初始状态,是必须的,不是可选择地
b.其次:如果s[i] == s[j] ,那么两端单独就可以构成回文子序列,而且与dp[i+1][j],dp[i][j-1],dp[i+1][j-1],中的回文序列又可以构成新的回文序列,所以此时dp[i][j] = dp[i+1][j] U dp[i][j-1] + dp[i+1][j-1] +1;而dp[i][j]已经更新为 dp[i+1][j] U dp[i][j-1],所以dp[i][j] = dp[i][j] + dp[i+1][j-1] +1;
状态转移方程:
dp[i][j]表示i~j内最多的回文字串数目
dp[i][j] = dp[i+1][j]+dp[i][j-1] -dp[i+1][j-1] (容斥)
if(s[i] == s[j]) dp[i][j] = dp[i][j] +dp[i+1][j-1] +1; (思维)
注:这里因为容斥时有减法,所以要先加上模再取模,要不会出现负数!
#include
using namespace std;
const int maxn = 1010;
const int mod = 10007;
int t, dp[maxn][maxn];
char ch[maxn];
int main()
{
scanf("%d", &t);
int tot = 0;
while(t--)
{
scanf("%s", ch + 1);
int n = strlen(ch + 1);
for(int i = 1; i <= n; i++) dp[i][i] = 1;
for(int len = 1; len <= n; len++)
{
for(int i = 1; i + len <= n + 1; i++)
{
int j = i + len - 1;
dp[i][j] = (dp[i][j - 1] + dp[i + 1][j] - dp[i + 1][j - 1]+ mod) % mod;
if(ch[i] == ch[j]) dp[i][j] = (dp[i][j] + dp[i + 1][j - 1] + 1) % mod;
}
}
printf("Case %d: %d\n", ++tot, dp[1][n]);
}
return 0;
}
题意:一个人参加party,然后需要不断的换衣服,而且它穿过的衣服就不能再穿,但是可以套衣服,问你他需要准备多少件衣服。
分析:有两种情况:
1.再新穿一件。
2.含有这件衣服脱到那一件。
然后求取区间内最大值。
状态转移方程:
1.穿一件新的 dp[i][j] = dp[i][j - 1] + 1;
2.第k件和需要的衣服相同。
if(a[k] == a[j]) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j - 1]);
#include
#include
#include
using namespace std;
const int maxn = 110;
int T, n, a[maxn], dp[maxn][maxn];
int main()
{
scanf("%d", &T);
int tot = 0;
while(T--)
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
dp[i][i] = 1; // 注意这里,第i个位置必须新穿一件
}
for(int i = 1; i <= n; i++)
for(int j = i; j <= n; j++)
dp[i][j] = j - i + 1;
for(int len = 1; len <= n; len++)
{
for(int i = 1; i + len <= n + 1; i++)
{
int j = i + len - 1;
dp[i][j] = dp[i][j - 1] + 1;
for(int k = 1; k < j; k++)
{
if(a[k] == a[j]) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j - 1]);
}
}
}
printf("Case %d: %d\n", ++tot, dp[1][n]);
}
return 0;
}
题意:给你一个合法的括号序列,要求你只能括号的一边染色,可以将其染成红色蓝色或者不染色,问你最大的可能情况是多少。
分析:我们要做的是对括号染不同的颜色,
因此可以有三种情况:
1. l+ 1 == r相邻,只能染一个,状态转移
dp[l][r][0][1] = 1; dp[l][r][1][0] = 1; dp[l][r][0][2] = 1; dp[l][r][2][0] = 1;
2.两边括号相匹配:
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
{
if(j != 1) dp[l][r][0][1] = (dp[l][r][0][1] + dp[l + 1][r - 1][i][j]) % mod;
if(i != 1) dp[l][r][1][0] = (dp[l][r][1][0] + dp[l + 1][r - 1][i][j]) % mod;
if(j != 2) dp[l][r][0][2] = (dp[l][r][0][2] + dp[l + 1][r - 1][i][j]) % mod;
if(i != 2) dp[l][r][2][0] = (dp[l][r][2][0] + dp[l + 1][r - 1][i][j]) % mod;
}
3.括号不匹配情况
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 3; j++)
{
for(int k = 0; k < 3; k++)
{
for(int h = 0; h < 3; h++)
{
if((k == 1 || k == 2) && (k == h)) continue;
dp[l][r][i][j] = (dp[l][r][i][j] + (dp[l][kk][k][j] * dp[kk + 1][r][i][h]) % mod) % mod;
}
}
}
}
代码:
#include
using namespace std;
const int maxn = 720;
const int mod = 1000000007;
char ch[maxn];
long long dp[maxn][maxn][3][3], num[maxn];
void match(int len)
{
stack sta;
for(int i = 1; i <= len; i++)
{
if(ch[i] == '(') sta.push(i);
else
{
num[i] = sta.top();
num[sta.top()] = i;
sta.pop();
}
}
}
void slove(int l, int r)
{
if(l + 1 == r)
{
dp[l][r][0][1] = 1;
dp[l][r][1][0] = 1;
dp[l][r][0][2] = 1;
dp[l][r][2][0] = 1;
return;
}
else if(num[l] == r)
{
slove(l + 1, r - 1);
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
{
if(j != 1) dp[l][r][0][1] = (dp[l][r][0][1] + dp[l + 1][r - 1][i][j]) % mod;
if(i != 1) dp[l][r][1][0] = (dp[l][r][1][0] + dp[l + 1][r - 1][i][j]) % mod;
if(j != 2) dp[l][r][0][2] = (dp[l][r][0][2] + dp[l + 1][r - 1][i][j]) % mod;
if(i != 2) dp[l][r][2][0] = (dp[l][r][2][0] + dp[l + 1][r - 1][i][j]) % mod;
}
}
else
{
int kk = num[l];
slove(l, kk); slove(kk + 1, r);
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
for(int k = 0; k < 3; k++)
for(int h = 0; h < 3; h++)
{
if((k == 1 || k == 2) && (k == h)) continue;
dp[l][r][i][j] = (dp[l][r][i][j] + (dp[l][kk][k][j] * dp[kk + 1][r][i][h]) % mod) % mod;
}
}
}
int main()
{
scanf("%s", ch + 1);
int n = strlen(ch + 1);
memset(dp, 0, sizeof(dp));
match(n);
slove(1, n);
long long ans = 0;
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
ans = (ans + dp[1][n][i][j]) % mod;
printf("%lld\n", ans);
return 0;
}