区间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的一个方案数\)
集合划分:这里无非就两种情况。

  1. (....),这个可以根据我们左右括号的颜色来确定我们可以划分的合法状态。也就是区间[l,r]可以往[l-1,r+1]划分。
  2. (...)(...),这个状态则可以划分为四个括号的不同颜色组合,但是我们需要注意这个状态必须得合法。
    这个虽然是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]的木棍染成正确颜色的集合\)

集合属性

\(最小操作数\)

集合划分

  1. \(s[l]==s[r]\).我们肯定是想把我们的区间缩小,那么前一个区间是哪一个呢,\(f[i+1][j]和f[i][j-1]\)是一个不错的选择。为什么可以转移到这里呢,因为这两个我们只要稍微多涂上那么一格就到了我们的\(f[i][j]\).
  2. \(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]这一段区间的数变为相同颜色的最小操作数。\)

集合划分

  1. \(color[i]==color[j]\).这个我们就一定需要理解好集合的定义了。这里我们可以从搜索的角度来理解问题吗,也就是数学归纳法:\(f[i][j]表示的是已经填成了相同颜色的一段区间\)

    比如 2 1 1 1 2
    我们可以把1全部都变为2.
    也即是操作一次之后变为了
    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;

}

总结

  1. 首先区间dp得看出来他是一个区间dp,那么怎么看出来他是一个区间dp呢。其实我们可以模拟下小样例,看我们的l,r是不是可以随着我们操作次数的增加而不断的变小。
  2. 区间dp的划分方式其实就只有两种:\(枚举断点k\)\(f[i+1][j]和f[i][j-1]\).
  3. 个人认为其实区间dp就是一个分治的过程,不断地通过缩小区间的范围,然后再由小范围的答案更新大范围的答案。这个有点类似于线段树把。其实当不好写的时候完全可以把它看作搜索来做。
posted @ 2023-01-03 00:36  努力的德华  阅读(42)  评论(0)    收藏  举报