区间dp

区间dp解决的问题是一个问题能分成好多个小区间,那么可以从小到大枚举区间长度
由小到大推出问题终解


区间 DP 有以下特点(摘自oiwiki):

合并:即将两个或多个部分进行整合,当然也可以反过来;

特征:能将问题分解为能两两合并的形式;

求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。


区间dp通常的套路是

  1. 从小到大枚举区间长度
  2. 枚举左端点
  3. 从左端点到右端点枚举区间划分位置k,来算出左端点到右端点这个区间的最大贡献

需要注意的是第三步枚举是从左端点枚举到右端点-1

前两步几乎都是这样,第三步因题而异,还要看具体题的状态转移
下面来几道题看一下区间dp的板子和应用方法


题目1 P1775 石子合并(弱化版)

https://www.luogu.com.cn/problem/P1775

主要是用这题看一下区间dp的板子

# include<bits/stdc++.h>

using namespace std;

int a[330],s[330],f[330][330];
int main()
{
    int n;cin>>n;
    for(int i=1;i<=n;i++) {scanf("%d",&a[i]);s[i] = s[i-1]+a[i];}
    memset(f,0x3f,sizeof f);
    for(int i=1;i<=n;i++) f[i][i] = 0;
    for(int k=1;k<=n;k++)
    {
        for(int l = 1;l+k-1<=n;l++)
        {
            for(int d = l;d<=l+k-1;d++)
            {
                f[l][l+k-1] = min(f[l][l+k-1],f[l][d]+f[d+1][l+k-1]+s[l+k-1]-s[l-1]);
            }
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

题目2 P1880 [NOI1995] 石子合并

https://www.luogu.com.cn/problem/P1880

和上一题唯一的区别是这次是环形,我们可以把前n个再复制一份放到最后,这样枚举的时候就能枚举到所有情况
因为要求最小和最大值,所以要算两次

# include<bits/stdc++.h>

using namespace std;

const int N = 510;
int a[N],f[N][N],s[N];

int main()
{
	int n;cin>>n;
	for(int i=1;i<=n;i++) {cin>>a[i];s[i] = s[i-1]+a[i];}
        int n1 = n;
        for(int i=1;i<n1;i++) {a[++n] = a[i];s[n] = s[n-1]+a[n];}
	memset(f,0x3f,sizeof f);
	for(int i=1;i<=n;i++) f[i][i] = 0;
	for(int i=1;i<=n;i++)
	{
            for(int j=1;j+i-1<=n;j++)
	    {
		for(int k=j;k<j+i-1;k++)
		{
	     	    f[j][j+i-1] = min(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+s[j+i-1]-s[j-1]); 
		}
	    }
	}
    int ans = 0x3f3f3f3f;
    for(int i=1;i+n1-1<=n;i++) ans = min(ans,f[i][i+n1-1]);
    cout<<ans<<endl;
    memset(f,0,sizeof f);
    for(int i=1;i<=n;i++)
    {
	for(int j=1;j+i-1<=n;j++)
	{
		for(int k=j;k<j+i-1;k++)
		{
		    f[j][j+i-1] = max(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]+s[j+i-1]-s[j-1]); 
		}
	}
     }
    ans = 0;
    for(int i=1;i+n1-1<=n;i++) ans = max(ans,f[i][i+n1-1]);
    cout<<ans<<endl;
    return 0;
}

题目3 P1063 [NOIP2006 提高组] 能量项链

https://www.luogu.com.cn/problem/P1063

和第二题几乎一模一样,只是改成乘法了,知道划分区间时是哪三个数相乘就很容易了

# include<bits/stdc++.h>
# define int unsigned long long
using namespace std;
typedef pair<int,int> pii;

const int N = 220;

pii a[N];
int f[N][N];

signed main()
{
	int n;cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].first;
	for(int i=1;i<=n;i++)
	{
		if(i<n) a[i].second = a[i+1].first;
		if(i == n) a[i].second = a[1].first;
	}
	int n1 = n;
	for(int i=1;i<n1;i++) a[++n] = a[i];
	for(int i=1;i<=n1;i++)
	{
		for(int l=1;l+i-1<=n;l++)
		{
			for(int k=l;k<l+i-1;k++)
			{
				f[l][l+i-1] = max(f[l][l+i-1],f[l][k]+f[k+1][l+i-1]+a[l].first*a[k].second*a[l+i-1].second);
			}
		}
	}
	int ans = 0;
	for(int i=1;i+n1-1<=n;i++) ans = max(ans,f[i][i+n1-1]);
	cout<<ans<<endl;
	return 0;
}

题目4 [USACO16OPEN] 248 G

https://www.luogu.com.cn/problem/P3146

题意:
给定一个 1*N的地图,在里面玩 2048,每次可以合并相邻两个(2≤N≤248),问序列中出现的最大数字的值最大是多少。注意合并后的数值并非加倍而是 +1,例如2与2 合并后的数值为3。

需要注意的是这次的dp数组f[i][j]表示的是区间i到j所有数能合成的最大数,如果不能是所有数都参与到合成就是0
所以最终答案不是f[1][n],而是要每次统计出能合成的数的最大值,找到出现的最大值就是答案

# include<bits/stdc++.h>

using namespace std;

const int N = 300;
int a[N];
int f[N][N];
int main()
{
	int n;cin>>n;
	int ans = 0;
	for(int i=1;i<=n;i++) {cin>>a[i];ans = max(ans,a[i]);}
	for(int i=1;i<=n;i++) f[i][i] = a[i];
	for(int i=1;i<=n;i++)
	{
	   for(int l=1;l+i-1<=n;l++)
	   {
		 for(int k=l;k<l+i-1;k++)
		 {
		     if(f[l][k] == f[k+1][l+i-1] && f[l][k]) 
                     f[l][l+i-1] = max(f[l][l+i-1],f[l][k]+1);
		     ans = max(ans,f[l][l+i-1]);
		}
	   }
        }
	cout<<ans<<endl;
	return 0;
}

题目5 [USACO16OPEN] 262144 P

https://www.luogu.com.cn/problem/P3147

题意:
Bessie被她最近玩的一款游戏迷住了,游戏一开始有n个正整数,(2<=n<=262144),范围在1-40。在一步中,贝西可以选相邻的两个相同的数,然后合并成一个比原来的大一的数(例如两个7合并成一个8),目标是使得最大的数最大,请帮助Bessie来求最大值。

这个和上一题的唯一区别就是数据范围2e2到2e5,那么区间dp这种n^3的做法肯定是行不通了

那么考虑一种新的状态转移方式
令f[i][j]表示为从坐标i开始如果能合成j,那么能到达的右端点+1
也就是左端点到右端点这个区间的所有数可以合成数字j,f[i][j] = 右端点+1
每次更新最大的能合成的数字

至于能合成的最大的数是怎么求出来的可以看n的范围 262144<2^20的,也就是合成的最大的数<40+20=60

# include<bits/stdc++.h>

using namespace std;

const int N = 262146;
int a[N];
int f[N][60];

int main()
{
	int n;cin>>n;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) f[i][a[i]] = i+1;
	int ans = 0;
	for(int i=2;i<60;i++)
	{ 
		for(int j=1;j<=n;j++) 
		{
			if(!f[j][i]) f[j][i] = f[f[j][i-1]][i-1];
 //如果f[j][i] = 0,那么证明以j为左端点的区间没有一个能将所有数合成后到达i
 //也自然不能从任何状态转移
			 if(f[j][i]) ans = max(ans,i);
		}
	}
	cout<<ans<<endl;
	return 0;
}
posted @ 2023-08-19 18:10  拾墨、  阅读(13)  评论(0编辑  收藏  举报