学习笔记:区间dp

概念

区间 d p dp dp,其实就是解决一个区间的一个最值的一种 d p dp dp

方法

这种类型的 d p dp dp一般都是用区间的左右边界作为状态,然后从中间找到一个中转的位置,分成几个区间分别解决。这种 d p dp dp为了保证先算子问题,所以一般会把区间的长度放在最外层。当然,区间 d p dp dp用记忆化搜索会更简单。

例题

AcWing 1068

这个题因为是求一个区间合并的最值,而且两个连着的区间可以轻易合并成一个区间,所以可以用区间 d p dp dp。设 f i , j f_{i,j} fi,j为区间 [ i , j ] [i,j] [i,j]合并成一个数所需的最小代价。则可以将其分为两个区间分别合并成一个数,再将两个区间合并出的数合并成一个数,则 f i , j = min ⁡ ( f i , k + f k + 1 , j + ∑ x = i j a x , i ≤ k ≤ j ) f_{i,j}=\min(f_{i,k}+f_{k+1,j}+\displaystyle\sum_{x=i}^ja_x,i\leq k\leq j) fi,j=min(fi,k+fk+1,j+x=ijax,ikj),但是本题的区间是环形的,所以可以再接一串到后面,这样所有情况就都考虑了。因为本题要求和,则可以前缀和来完成该操作。为了保证先算某个状态的子问题,则第一层循环按长度枚举。最大代价同理。

#include<bits/stdc++.h>
using namespace std;
const int NN=204;
int a[NN],f1[NN][NN],f2[NN][NN],x[NN];
int main()
{
	int n,ans1=1e9,ans2=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&x[i]);
		a[i]=a[i-1]+x[i];
	}
	for(int i=n+1;i<=n*2;i++)
		a[i]=a[i-1]+x[i-n];
	for(int s=2;s<=n;s++)
		for(int i=1;i<=2*n-s+1;i++)
		{
			int j=i+s-1;
			f1[i][j]=1e9;
			for(int k=i;k<j;k++)
			{
				f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]+a[j]-a[i-1]);
				f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]+a[j]-a[i-1]);
			}
		}
	for(int i=1;i<=n;i++)
	{
		ans1=min(ans1,f1[i][i+n-1]);
		ans2=max(ans2,f2[i][i+n-1]);
	}
	printf("%d\n%d",ans1,ans2);
	return 0;
}

AcWing 320

这个题是环形的,首先处理环就是再接一段。然后考虑本题的操作,发现这个题就是把一些东西合并到一起,每次都有一个代价,求代价的最大值,而且必须是连续的。这样刚好满足区间 d p dp dp的要求,则本题可以用区间 d p dp dp解。设 f i , j f_{i,j} fi,j i i i j j j的区间内合并成一个的最大代价,因为 r i = l i + 1 r_i=l_{i+1} ri=li+1,则有状态转移方程 f i , j = max ⁡ ( f i , k + f k + 1 , j + a i × a k + 1 × a j + 1 ) f_{i,j}=\max(f_{i,k}+f_{k+1,j}+a_i\times a_{k+1}\times a_{j+1}) fi,j=max(fi,k+fk+1,j+ai×ak+1×aj+1)

#include<iostream>
using namespace std;
const int NN=204;
int a[NN],f[NN][NN];
int main()
{
	int n,ans=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		a[i+n]=a[i];
	}
	for(int s=1;s<=2*n-1;s++)
		for(int i=1;i<=n*2-s;i++)
		{
			int j=i+s;
			for(int k=i;k<j;k++)
				f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*a[k+1]*a[j+1]);
		}
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][i+n-1]);
	printf("%d",ans);
	return 0;
}

AcWing 479

这个题目发现,因为给出的是中序遍历,则选中点 x x x为该子树的根,左边为左子树,右边为右子树,相当于分成了左右两部分又分别计算。我们惊奇地发现:这和区间 d p dp dp很像啊!而且要求的最值分值也十分符合区间 d p dp dp的性质,就是左区间的分值乘右区间的分值加上自己的分值。那么要求方案怎么办?从哪里转移的就相当于以哪里为根,记录一下从哪里转移的然后递归计算即可。某个点可能没有左子树或右子树,按题目要求没有子树则该子树分值为 1 1 1,则 f i + 1 , i = 1 f_{i+1,i}=1 fi+1,i=1。最后,状态转移方程不必多讲, f i , j = max ⁡ ( f i , k − 1 × f k + 1 , j + a k , i ≤ k ≤ j ) f_{i,j}=\max(f_{i,k-1}\times f_{k+1,j}+a_k,i\leq k\leq j) fi,j=max(fi,k1×fk+1,j+ak,ikj)

#include<bits/stdc++.h>
using namespace std;
const int NN=34;
int f[NN][NN],pre[NN][NN]; 
void print(int l,int r)
{
	if(l>r)
		return;
	printf("%d ",pre[l][r]);
	print(l,pre[l][r]-1);
	print(pre[l][r]+1,r);
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&f[i][i]);
		pre[i][i]=i;
		f[i+1][i]=1;
    }
	f[1][0]=1;
	for(int s=2;s<=n;s++)
		for(int i=1;i+s-1<=n;i++)
		{
		    int j=i+s-1;
			for(int k=i;k<=j;k++)
				if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k])
				{
					f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k];
					pre[i][j]=k;
				}
		}
	printf("%d\n",f[1][n]);
	print(1,n);
	return 0;
}

AcWing 1069

本题看着毫无头绪,但是很容易想出来,每一次一定会划分出两个多边形。但是即使这样,也不知道该以哪几个顶点划分。怎么办呢?因为如果要把多边形划分成三角形,则多边形的每一条边都会在划分的某一个三角形上。于是对于整个多边形,我们可以先将包含 1 , n 1,n 1,n这条棱的三角形划分出来。但是还缺少一个顶点 k k k,则可以枚举剩下的顶点 k k k。不难发现,如果用一个 k k k割开,则会分割出来两个互不干扰的图形,第一个图形中点的编号为 1... k 1...k 1...k,第二个图形中点的编号为 k . . . n k...n k...n。将其一般化,设 f i , j f_{i,j} fi,j为点的编号为 i . . . j i...j i...j的多边形划分的最小代价,则有状态转移方程 f i , j = a i × a j × a k + f i , k + f k , j f_{i,j}=a_i\times a_j\times a_k+f_{i,k}+f_{k,j} fi,j=ai×aj×ak+fi,k+fk,j。因为本题数据范围极大,需要高精度计算。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NN=54,MM=34;
int w[NN];
ll f[NN][NN][MM];
void add(ll a[],ll b[])
{
    int t=0;
    for(int i=0;i<MM;i++)
    {
        t+=a[i]+b[i];
        a[i]=t%10;
        t/=10;
    }
}
void mul(ll a[],ll b)
{
    ll t=0;
    for(int i=0;i<MM;i++)
    {
        t+=a[i]*b;
        a[i]=t%10;
        t/=10;
    }
}
int cmp(ll a[],ll b[])
{
    for(int i=MM-1;i>=0;i--)
        if(a[i]>b[i])
            return 1;
        else if(a[i]<b[i])
            return -1;
    return 0;
}
void print(ll a[])
{
    int k=MM-1;
    while(k&&!a[k])
        k--;
    while(k>=0)
        printf("%d",a[k--]);
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    ll temp[MM];
    for(int len=3;len<=n;len++)
        for(int l=1;l+len-1<=n;l++)
        {
            int r=l+len-1;
            f[l][r][MM-1]=1;
            for(int k=l+1;k<r;k++)
            {
                memset(temp,0,sizeof(temp));
                temp[0]=w[l];
                mul(temp,w[k]);
                mul(temp,w[r]);
                add(temp,f[l][k]);
                add(temp,f[k][r]);
                if(cmp(f[l][r],temp)>0)
                    memcpy(f[l][r],temp,sizeof(temp));
            }
        }
    print(f[1][n]);
    return 0;
}

AcWing 321

本题需要计算某个区间的价值和,首先知道肯定要前缀和求价值。能求价值了,接下来考虑,发现要把某个地方割开,这正好和区间 d p dp dp找一个 k k k分割出两个区间求最大值很像。于是,我们就只需要知道是哪个矩阵就可以枚举怎么切了。因为本题是二维的,所以要分别枚举横着或竖着切。设 f i 1 , j 1 , i 2 , j 2 , k f_{i1,j1,i2,j2,k} fi1,j1,i2,j2,k表示矩阵的左上角和右下角的坐标以及还能切的次数,然后枚举在矩阵中怎么切割。因为本题状态十分混乱,所以最好记忆化搜索。以上就是二维的区间 d p dp dp,二维 d p dp dp中需要枚举横着和竖着的情况,分割出来的是两个矩阵。

#include<bits/stdc++.h>
using namespace std;
const int NN=9;
int n,s[NN][NN];
double f[NN][NN][NN][NN][19];
double get(int x1,int y1,int x2,int y2)
{
    double sum=(double)(s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1])-1.0*s[8][8]/n;
    return sum*sum/n;
}
double dp(int x1,int y1,int x2,int y2,int k)
{
    if(f[x1][y1][x2][y2][k]>=0)
        return f[x1][y1][x2][y2][k];
    if(k==1)
        return get(x1,y1,x2,y2);
    f[x1][y1][x2][y2][k]=1e9;
    for(int i=x1;i<x2;i++)
    {
        f[x1][y1][x2][y2][k]=min(f[x1][y1][x2][y2][k],dp(x1,y1,i,y2,k-1)+get(i+1,y1,x2,y2));
        f[x1][y1][x2][y2][k]=min(f[x1][y1][x2][y2][k],dp(i+1,y1,x2,y2,k-1)+get(x1,y1,i,y2));
    }
    for(int i=y1;i<y2;i++)
    {
        f[x1][y1][x2][y2][k]=min(f[x1][y1][x2][y2][k],dp(x1,y1,x2,i,k-1)+get(x1,i+1,x2,y2));
        f[x1][y1][x2][y2][k]=min(f[x1][y1][x2][y2][k],dp(x1,i+1,x2,y2,k-1)+get(x1,y1,x2,i));
    }
    return f[x1][y1][x2][y2][k];
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=8;i++)
        for(int j=1;j<=8;j++)
            scanf("%d",&s[i][j]);
    for(int i=1;i<=8;i++)
        for(int j=1;j<=8;j++)
            s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
    memset(f,-1,sizeof f);
    printf("%.3lf\n",sqrt(dp(1,1,8,8,n)));
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值