区间dp
A
核心思路:这很明显是一道区间dp板子题
集合定义:\(f[i][j]表示的是将序列的第i个数合并到第j个数\)全部合并所能得到的最大值。
集合划分:和石子合并的区间划分是一样的哦,先枚举长度,然后枚举左端点那么右端点也就确定了。然后我们再对区间里进行划分,这就是\(f[i][j]\)的子集。要注意有些状态是不合法的哦,划分的左右区间合并之后的是相等的才是合法的状态,也就是\(f[l][k]==f[k+1][r]\).
状态转移方程:
\(f[i][j]=max(f[i][j],f[i][k]+1)\).其中\(f[i][k]+1\)是因为这是题目要求的,需要我们两个相等的合并之后就是左区间或者是右区间的加1.
这里有个处理得比较好的地方就是f数组的储存,可以查看内存看一下。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 3e2 + 10;
int f[N][N];
int n;
int main()
{
int n;
int ans = 0;
cin >> n;
for (int i = 1;i <= n;i++)
{
scanf("%d", f[i] + i);//表示只有一个元素的情况.
ans = max(ans, f[i][i]);
}
for (int len = 2;len <= n;len++)
{
for (int l = 1;l + len - 1 <= n;l++)
{
int r = l + len - 1;
for (int k = l;k < r;k++)
{
if (f[l][k] == f[k + 1][r] && f[l][k])
{
f[l][r] = max(f[l][r], f[l][k] + 1);
ans = max(ans, f[l][r]);
}
}
}
}
cout << ans << endl;
}
B
核心思路: 其实这个题目本身并不是很难,不好处理的地方就是读题以及题目的读入。我认为这是本题最难处理的地方。这里还有一个需要注意的地方是我们这是一个环,所以我们采用断环为链的做法,也就是将两个长度为n的拼接在一起处理,经典的区间dp处理方法。
集合定义:\(f[i][j]为合并i和j这个区间所能得到的最大得分数\)
集合划分:又是和石子划分是一样的。这里就不进行赘述了。因为区间dp的划分好像都是一样的,没什么特别大的区别,区别主要在于状态转移方程。
状态转移方程:
这里我们就需要注意数据范围了,因为这有些数是负数所以我们在做乘法的时候需要注意。先举个例子吧,就比如:-5 -8.他们这两个值都是比较小的,但是乘起来却又很大。所以我们需要开一个g数组来存储我们区间[i,j]的最小值。我们做max的话就会有\(A_{2}^{2}=4\)中情况,所以枚举四种情况的max就好了。g数组也是这么更新的。
具体的可以看ac代码。
下面聊一聊这个状态的初始化,其实这个就和我们的递归找递归出口是一样的。先都初始化为无穷。然后考虑递归出口,也就是区间长度是1的情况。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e3 + 10,INF=0x3f3f3f3f;
int n, ans = -INF;
int a[N];
int f[N][N], g[N][N];
char c[N];
int main()
{
scanf("%d\n", &n);
for (int i = 1;i <= n;i++)
{
scanf("%c %d", &c[i], &a[i]);
getchar();//getchar是读入空格的。因为中间还夹杂着一个空格。
a[n + i] = a[i];
c[n + i] = c[i];
}
for (int i = 1;i <= 2 * n;i++)
{
for (int j = 1;j <= 2 * n;j++)
f[i][j] = -INF, g[i][j] = INF;
}
for (int i = 1;i <= 2 * n;i++)
{
f[i][i] = g[i][i] = a[i];
}
for (int len = 2;len <= n;len++)
{
for (int l = 1;l + len - 1 <= 2*n;l++)
{
int r = l + len - 1;
for (int k = l;k < r;k++)
{
if (c[k + 1] == 'x')
{
f[l][r] = max(f[l][r], max(f[l][k] * f[k + 1][r], max(g[l][k] * g[k + 1][r], max(f[l][k] * g[k + 1][r], g[l][k] * f[k + 1][r]))));
g[l][r] = min(g[l][r], min(f[l][k] * f[k + 1][r], min(g[l][k] * g[k + 1][r], min(f[l][k] * g[k + 1][r], g[l][k] * f[k + 1][r]))));
}
else if (c[k + 1] == 't')
{
f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
g[l][r] = min(g[l][r], g[l][k] + g[k + 1][r]);
}
}
}
}
for (int l = 1;l <= n;l++)
{
int r = l + n - 1;
ans = max(ans, f[l][r]);
}
cout << ans << endl;
for (int l = 1;l <= n;l++)
{
int r = l + n - 1;
if (f[l][r] == ans)
printf("%d ", l);
}
return 0;
}
C
题目链接
核心思路
集合定义:\(f[l][r][i][j]表示的是区间的左端点是l,右端点是r。并且左端点的颜色是i,右端点的颜色是j的一个方案数\)。
集合划分:这里无非就两种情况。
- (....),这个可以根据我们左右括号的颜色来确定我们可以划分的合法状态。也就是区间[l,r]可以往[l-1,r+1]划分。
- (...)(...),这个状态则可以划分为四个括号的不同颜色组合,但是我们需要注意这个状态必须得合法。
这个虽然是dp,但是我们当作记忆化搜索来做更加好理解,因为其实记忆化搜索的本质就是dp。但是既然是记忆化我们就还需要特殊处理下这种情况:().
然后就是还需要预处理出来match数组,这个因为是括号匹配所以我们可以使用栈来预处理出来这个match数组。
// Problem: Coloring Brackets
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF149D
// Memory Limit: 250 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define NO {puts("NO") ; return ;}
#define YES {puts("YES") ; return ;}
#define endl "\n"
#define int long long
const int maxn=710;
const int mod=1e9+7;
int n,match[maxn];
long long dp[maxn][maxn][3][3];
char s[maxn];
stack <int> St;
void dfs(int l,int r)
{
if(l+1==r)
{
dp[l][r][0][1]=dp[l][r][1][0]=dp[l][r][2][0]=dp[l][r][0][2]=1;
}
else if(match[l]==r)
{
dfs(l+1,r-1);
for(int i=0;i<=2;i++)
for(int j=0;j<=2;j++)
{
if(j!=1)
dp[l][r][0][1]=(dp[l][r][0][1]+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!=1)
dp[l][r][1][0]=(dp[l][r][1][0]+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
{
dfs(l,match[l]),dfs(match[l]+1,r);
for(int i=0;i<=2;i++)
for(int j=0;j<=2;j++)
{
for(int p=0;p<=2;p++)
for(int q=0;q<=2;q++)
{
if((j==1&&p==1)||(j==2&&p==2))//j和p表示的是两个相邻的括号
continue;
dp[l][r][i][q]=(dp[l][r][i][q]+dp[l][match[l]][i][j]*dp[match[l]+1][r][p][q]%mod)%mod;
}
}
}
}
signed main()
{
cin>>s;
n=strlen(s);
for(int i=0;i<n;i++)
{
if(s[i]=='(')
{
St.push(i);
}
else
{
match[St.top()]=i;
match[i]=St.top();
St.pop();
}
}
dfs(0,n-1);
int ans=0;
for(int i=0;i<=2;i++)
for(int j=0;j<=2;j++)
ans=(ans+dp[0][n-1][i][j])%mod;
cout<<ans<<endl;
}
C
核心思路
其实只要看懂题意就比较简单了,这个很显然是一个区间dp,因为我们发现变量就可以描述为左端点和右端点。然后按照题目意思算就好了。其实这里是允许我们一行一行的选的。就是一行一行的dp,别忘记初始化dp数组。
唯一需要注意的是我们是从大区间推导小区间,所以枚举的顺序别搞错了。
然后其实这个题目最麻烦的地方在于需要手写一个高精度,这里直接粘贴板子算了。
// Problem: P1005 [NOIP2007 提高组] 矩阵取数游戏
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1005
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define NO {puts("NO") ; return ;}
#define YES {puts("YES") ; return ;}
#define endl "\n"
#define int long long
const int MAXN = 85, Mod = 10000; //高精四位压缩大法好
int n, m;
int ar[MAXN];
struct HP {
int p[505], len;
HP() {
memset(p, 0, sizeof p);
len = 0;
} //这是构造函数,用于直接创建一个高精度变量
void print() {
printf("%d", p[len]);
for (int i = len - 1; i > 0; i--) {
if (p[i] == 0) {
printf("0000");
continue;
}
for (int k = 10; k * p[i] < Mod; k *= 10)
printf("0");
printf("%d", p[i]);
}
} //四位压缩的输出
} f[MAXN][MAXN], base[MAXN], ans;
HP operator + (const HP &a, const HP &b) {
HP c; c.len = max(a.len, b.len); int x = 0;
for (int i = 1; i <= c.len; i++) {
c.p[i] = a.p[i] + b.p[i] + x;
x = c.p[i] / Mod;
c.p[i] %= Mod;
}
if (x > 0)
c.p[++c.len] = x;
return c;
} //高精+高精
HP operator * (const HP &a, const int &b) {
HP c; c.len = a.len; int x = 0;
for (int i = 1; i <= c.len; i++) {
c.p[i] = a.p[i] * b + x;
x = c.p[i] / Mod;
c.p[i] %= Mod;
}
while (x > 0)
c.p[++c.len] = x % Mod, x /= Mod;
return c;
} //高精*单精
HP max(const HP &a, const HP &b) {
if (a.len > b.len)
return a;
else if (a.len < b.len)
return b;
for (int i = a.len; i > 0; i--)
if (a.p[i] > b.p[i])
return a;
else if (a.p[i] < b.p[i])
return b;
return a;
} //比较取最大值
void BaseTwo() {
base[0].p[1] = 1, base[0].len = 1;
for (int i = 1; i <= m + 2; i++){ //这里是m! m! m! 我TM写成n调了n年...
base[i] = base[i - 1] * 2;
}
} //预处理出2的幂
signed main()
{
cin>>n>>m;
BaseTwo();
while(n--)
{
memset(f,0,sizeof f);
for(int i=1;i<=m;i++)
cin>>ar[i];
for(int i=1;i<=m;i++)
{
for(int j=m;j>=i;j--)
{
f[i][j]=max(f[i][j],f[i-1][j]+base[m-j+i-1]*ar[i-1]);
f[i][j]=max(f[i][j],f[i][j+1]+base[m-j+i-1]*ar[j+1]);
}
}
HP Max;
for(int i=1;i<=m;i++)//最后一个数还没有算呢。
Max=max(Max,f[i][i]+base[m]*ar[i]);
ans=ans+Max;
}
ans.print();
return 0;
}
D
核心思路
集合定义
\(f[i][j]表示的是将长度为[l,r]的木棍染成正确颜色的集合\)
集合属性
\(最小操作数\)
集合划分
- \(s[l]==s[r]\).我们肯定是想把我们的区间缩小,那么前一个区间是哪一个呢,\(f[i+1][j]和f[i][j-1]\)是一个不错的选择。为什么可以转移到这里呢,因为这两个我们只要稍微多涂上那么一格就到了我们的\(f[i][j]\).
- \(s[l]!=s[r]\).那我们就开启分治的做法,其实也就是枚举分段点就好了。
初始化
首先因为需要求最小值,所以肯定先得初始化为正无穷。然后把我们长度为1的递归出口处理下就好了。
// Problem: P4170 [CQOI2007]涂色
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4170
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define NO {puts("NO") ; return ;}
#define YES {puts("YES") ; return ;}
#define endl "\n"
#define int long long
const int N=100;
int f[N][N];
signed main()
{
string s;
cin>>s;
int n=s.size();
memset(f,0x7F,sizeof(f));
for(int i=0;i<n;i++)
f[i][i]=1;
for(int len=2;len<=n;len++)//千万不可以从1开始因为1已经更新过了。
{
for(int i=0;i+len-1<n;i++)
{
int j=i+len-1;
if(s[i]==s[j])
{
f[i][j]=min(f[i+1][j],f[i][j]);
f[i][j]=min(f[i][j-1],f[i][j]);
}
else
{
for(int k=i;k<=j;k++)
f[i][j]=min(f[i][k]+f[k+1][j],f[i][j]);
}
}
}
cout<<f[0][n-1]<<endl;
return 0;
}
E
核心思路
这个虽然看上去和上面那个差不多,但是其实是有区别的。这个题意可能有一点难懂,但是其实也很简单,其实就是比如2 2 2这样一个连续相同的序列我们就可以把他们合并为一种颜色。
集合定义
\(f[i][j]表示的是[i,j]这一段区间的数变为相同颜色的最小操作数。\)
集合划分
-
\(color[i]==color[j]\).这个我们就一定需要理解好集合的定义了。这里我们可以从搜索的角度来理解问题吗,也就是数学归纳法:\(f[i][j]表示的是已经填成了相同颜色的一段区间\)。
比如 2 1 1 1 2 我们可以把1全部都变为2. 也即是操作一次之后变为了 2 2 2 2 2 -
如果不相等直接看前一个状态就好了,也就是经典的\(f[i+1][j]和f[i][j-1].\)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 5e3+ 5;
int f[N][N];
int n, idx;
int color[N];
int main()
{
cin >> n;
int last = -1;
for (int i = 1;i <= n;i++)
{
int x;
cin >> x;
if (last == -1)
{
color[++idx] = x;
last=x;
}
else
{
if (last != x)
{
color[++idx] = x;
last = x;
}
}
}
// for(int i=1;i<=idx;i++)
// cout<<color[i]<<" ";
// cout<<endl;
n = idx;
memset(f, 0x3f, sizeof f);
for (int i = 1;i <= n;i++)
{
f[i][i] = 1;
}
for (int len = 1;len <= n;len++)
{
for (int i = 1;i + len - 1 <= n;i++)
{
int j = i + len - 1;
if (color[i] == color[j]) {
f[i][j] = min(f[i][j], f[i + 1][j - 1] + 1);
}
else
{
f[i][j] = min(f[i][j], f[i + 1][j] + 1);
f[i][j] = min(f[i][j], f[i][j - 1] + 1);
}
}
}
cout << f[1][n] -1<< endl;//这里为什么需要减1我也解释不清楚,hhhh.
return 0;
}
F
核心思路
首先我们发现这个题有点难想到区间dp,但是我们可以发现他的边界l,r是可以不停的缩小的。所以说明这个题目还是可以使用区间dp来做的。
集合定义
\(f[i][j]表示的是区间[i,j]折叠后的最小的长度。\)
集合划分
首先需要注意的是这个和我们寻常的划分不一样,也就是不可以划分到\(f[i+1][j],f[i][j-1]\)这个状态。而是需要我们枚举断点k,来进行一个分治。
要是我们已经把我们区间缩小到了一定的范围呢,那我们就需要加一个check函数来检查这个字符串是不是可以折叠。
就拿字符串ABCABCABC举例子吧:
我们发现可以折叠为3(ABC)
所以我们再去找ABC看是否可以划分就好了,其中这个3是l/len,l表示的是总长度,len表示的是
ABC的长度。所以我们可以发现整个过程也即是一个不断地递归分治的过程。
// Problem: P4302 [SCOI2003]字符串折叠
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4302
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define NO {puts("NO") ; return ;}
#define YES {puts("YES") ; return ;}
#define endl "\n"
#define int long long
const int N=110;
int f[N][N];
int m[10*N];
string s;
int n;
int check(int l,int r,int len)
{
for(int i=l;i<=r;i++)
{
if(s[i]!=s[(i-l)%len+l])//画一个ABCABCABC模拟下就知道了。
return 0;
}
return 1;
}
void Init()//m数组表示的是折叠的长度。
{
for(int i=0;i<=9;i++)
m[i]=1;
for(int i=10;i<=99;i++)
m[i]=2;
for(int i=100;i<=999;i++)
m[i]=3;
}
signed main()
{
Init();
cin>>s;
n=s.size();
s=' '+s;
memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++)
f[i][i]=1;
for(int l=2;l<=n;l++)//一定要注意这个l表示的是长度。
{
for(int i=1,j=i+l-1;j<=n;i++,j++)
{
for(int k=i;k<j;k++)
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
for(int k=i;k<j;k++)
{
int len=k-i+1;
if(l%len)
continue;
if(check(i,j,len))
f[i][j]=min(f[i][j],f[i][k]+m[l/len]+2);
}
}
}
cout<<f[1][n]<<endl;
return 0;
}
总结
- 首先区间dp得看出来他是一个区间dp,那么怎么看出来他是一个区间dp呢。其实我们可以模拟下小样例,看我们的l,r是不是可以随着我们操作次数的增加而不断的变小。
- 区间dp的划分方式其实就只有两种:\(枚举断点k\),\(f[i+1][j]和f[i][j-1]\).
- 个人认为其实区间dp就是一个分治的过程,不断地通过缩小区间的范围,然后再由小范围的答案更新大范围的答案。这个有点类似于线段树把。其实当不好写的时候完全可以把它看作搜索来做。

浙公网安备 33010602011771号