前缀和优化DP详细总结


一、连续子段和

1.两段长度为k的不相交区间连续和最大值

题目链接

思路:因为要求两段连续不相交且长度为k的区间最大值,我们可以预处理出i后边的区间的最大区间值,用 f [ i ] f[i] f[i]表示,类似于前缀和。
然后正着遍历一遍,找到i前边包括i的区间长度为k的最大值
更新答案
r e s = m a x ( r e s , m a x v + f [ i + 1 ] ) res=max(res,maxv+f[i+1]) res=max(res,maxv+f[i+1])

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 200010,INF=1e18;
int s[N];
int n,k;
int f[N];
signed main()
{
    int T;
    cin>>T;
    while(T--)
    {
        cin>>n>>k;
        for(int i=1;i<=n;i++)cin>>s[i],s[i]+=s[i-1];
        for(int i=0;i<=n;i++)f[i]=-INF;
        for(int i=n-k+1;i>=k;i--)f[i]=max(f[i+1],s[i+k-1]-s[i-1]);
        int res=-INF,maxv=-INF;
        for(int i=k;i<=n-k;i++)
        {
            maxv=max(maxv,s[i]-s[i-k]);
            res=max(res,maxv+f[i+1]);
        }
        cout<<res<<endl;
    }
}

2.两段不限制长度的不相交区间连续和最大值

题目链接

思路:
f [ i ] : f[i]: f[i]: 表示前i个值以i为右端点的最大区间和
g [ i ] : g[i]: g[i]: 表示从in的区间中以i为左端点的最大区间和
根据贪心思路,如果前边区间和为负数就不加上,为正数就加上
F [ i ] : F[i]: F[i]:i个数中最大区间和,即前i个数中最大的f[i]
G [ i ] : G[i]: G[i]: in中最大区间和,即in中最大的g[i]

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 50010;
int f[N];//f[i]表示前i个值以i为右端点的最大区间和
int g[N];//g[i]表示从i到n的区间中以i为左端点的最大区间和
//根据贪心思路,如果前边区间和为负数就不加上,为正数就加上
int F[N];//前i个数中最大区间和,即前i个数中最大的f[i]
int G[N];//i到n中最大区间和,即i到n中最大的g[i]
int w[N];
int n;
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&n);
        F[0]=G[n+1]=-0x3f3f3f3f;
        for(int i=1;i<=n;i++)scanf("%d",w+i);
        for(int i=1;i<=n;i++)
        {
            if(i==1)f[i]=w[i];
            else f[i]=max(f[i-1],0)+w[i];
            F[i]=max(F[i-1],f[i]);
        }
        for(int i=n;i>=1;i--)
        {
            if(i==n)g[i]=w[i];
            else g[i]=max(g[i+1],0)+w[i];
            G[i]=max(G[i+1],g[i]);
        }
        int res=-0x3f3f3f3f;
        for(int i=1;i<n;i++)
        {
            res=max(res,F[i]+G[i+1]);
        }
        printf("%d\n",res);
    }
}

3. m ( m > 2 ) m(m>2) m(m>2)段长度为k的不相交区间连续和最大值

这个没找到例题,只把思路写上吧,遇到例题的话再补上
思路:
状态表示:

f [ i ] [ j ] f[i][j] f[i][j]表示前 i 个数已经选了 j 个长度为k 的不相交区间的最大权值和

状态计算:

f [ i ] [ j ] = m a x ( f [ i − k ] [ j − 1 ] + s u m [ i ] − s u m [ i − k ] , f [ i − 1 ] [ j ] ) f[i][j]=max(f[i-k][j-1]+sum[i]-sum[i-k],f[i-1][j]) f[i][j]=max(f[ik][j1]+sum[i]sum[ik],f[i1][j])

4. m ( m > 2 ) m(m>2) m(m>2)段不限制长度的不相交区间连续和最大值

对于一维状态来说
状态表示:

g [ i ] [ j ] [ 0 / 1 ] g[i][j][0/1] g[i][j][0/1]表示前 i 个数,分成 j 段,第 i个不选/选的最大值

状态计算:

g [ i ] [ j ] [ 0 ] = m a x ( g [ i − 1 ] [ j ] [ 0 ] , g [ i − 1 ] [ j ] [ 1 ] ) g[i] [j] [0]=max(g[i-1] [j] [0],g[i-1] [j] [1]) g[i][j][0]=max(g[i1][j][0],g[i1][j][1])
g [ i ] [ j ] [ 1 ] = m a x ( g [ i − 1 ] [ j ] [ 1 ] , g [ i − 1 ] [ j − 1 ] [ 1 ] , g [ i − 1 ] [ j − 1 ] [ 0 ] ) + a [ i ] g[i][j][1]=max(g[i-1] [j] [1],g[i-1] [j-1] [1],g[i-1] [j-1] [0])+a[i] g[i][j][1]=max(g[i1][j][1],g[i1][j1][1],g[i1][j1][0])+a[i]

在写这个题之前先来一个简化版

题目链接

暴力做法: O ( n 4 ) O(n^4) O(n4)
预处理二维前缀和,枚举矩阵的左上角和右下角两个位置,计算中间最大值

这里用贪心思路进行优化: O ( n 3 ) O(n^3) O(n3)
思路:

用暴力做法可以发现其实每次计算的时候每个子矩阵会有很多重复的位置
因此想办法把这些位置消掉
首先预处理每一列的前缀和,然后直接枚举子矩阵对应的上顶和下底对应的行数,在枚举列数,根据贪心思想计算就可以了

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int a[N][N]={0};
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            cin>>a[i][j];
            a[i][j]+=a[i-1][j];
        }
    
    int res=-0x3f3f3f3f;
    for(int i=1;i<=n;i++)
    {
        for(int j=i;j<=n;j++)
        {
            int last=0;
            for(int k=1;k<=n;k++)
            {
                last=max(0,last)+a[j][k]-a[i-1][k];
                res=max(last,res);
            }
        }
    }
    cout<<res<<endl;
}

进入正题:

最大子矩阵—两列数中选k个子矩阵和最大(难)

题目链接

题解摘自:qcjj题解

m=1是最大连续k段和
m=2时: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示第一列的前i个数,第二列的前j个数总共选了k个子矩形的答案,枚举最后一个区间是怎么选的:
(再用一个sum[i][1/2]表示第1/2列的前缀和)
i==j时,可能能有两列选到一个矩形的情况:

f [ i ] [ j ] [ k ] = m a x ( f [ x ] [ x ] [ k ] + s u m [ i ] [ 1 ] − s u m [ x − 1 ] [ 1 ] + s u m [ i ] [ 2 ] − s u m [ x − 1 ] [ 2 ] ) f[i][j][k] = max(f[x][x][k] + sum[i][1] - sum[x-1][1] + sum[i][2] - sum[x-1][2]) f[i][j][k]=max(f[x][x][k]+sum[i][1]sum[x1][1]+sum[i][2]sum[x1][2])

仅在第一列选一个区间:

f [ i ] [ j ] [ k ] = m a x ( f [ x ] [ j ] [ k − 1 ] + s u m [ i ] [ 1 ] − s u m [ x − 1 ] [ 1 ] ) f[i][j][k]=max(f[x][j][k-1]+sum[i][1]-sum[x-1][1]) f[i][j][k]=max(f[x][j][k1]+sum[i][1]sum[x1][1])

仅在第二列选一个区间:

f [ i ] [ j ] [ k ] = m a x ( f [ i ] [ x ] [ k − 1 ] + s u m [ j ] [ 2 ] − s u m [ x − 1 ] [ 2 ] ) f[i][j][k]=max(f[i][x][k-1]+sum[j][2]-sum[x-1][2]) f[i][j][k]=max(f[i][x][k1]+sum[j][2]sum[x1][2])

什么都不选:

f [ i ] [ j ] [ k ] = m a x ( f [ i − 1 ] [ j ] [ k ] , f [ i ] [ j − 1 ] [ k ] ) f[i][j][k]=max(f[i-1][j][k],f[i][j-1][k]) f[i][j][k]=max(f[i1][j][k],f[i][j1][k])

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 110;
int n,m,k,t;
int f[N][N][15],a[N][3];
int main(){
	cin>>n>>m>>t;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>k; 
			a[i][j]=a[i-1][j]+k;//每一列的前缀和 
		}
	}
	for(int i=1;i<=n;i++){//第一列
		for(int j=1;j<=n;j++){//第二列
			for(int k=1;k<=t;k++){
				f[i][j][k]=max(f[i-1][j][k],f[i][j-1][k]);//都不选时
				//只在第1列选时 
				for(int x=0;x<i;x++) f[i][j][k]=max(f[i][j][k],f[x][j][k-1]+a[i][1]-a[x][1]);
				//只在第2列选时 
				for(int y=0;y<j;y++) f[i][j][k]=max(f[i][j][k],f[i][y][k-1]+a[j][2]-a[y][2]);
				if(i!=j) continue;
				//如果i==j,可能会出现在两列选一个矩形的情况 
				for(int x=0;x<i;x++) f[i][j][k]=max(f[i][j][k],f[x][x][k-1]+a[i][1]-a[x][1]+a[i][2]-a[x][2]);
			}
		}
	}
	cout<<f[n][n][t]<<endl;
}

二、普通DP优化

正常暴力做法的话都会TLE

主要思想:用前缀和减少一维for循环,减少时间复杂度,使 O ( n ) O(n) O(n)变为 O ( 1 ) O(1) O(1)

如果计算 d p [ i ] dp[i] dp[i] 时需要一步求和 s u m ( d p [ a . . b ] ) sum(dp[a..b]) sum(dp[a..b]),如果用遍历的方式,则转移的时间复杂度为 O ( N ) O(N) O(N)

如果在状态转移的过程中维护一个 dp 数组的前缀和 sum,则这步求和可以用 s u m [ b + 1 ] − s u m [ a ] sum[b + 1] - sum[a] sum[b+1]sum[a] 直接得到,转移的时间复杂度变为 O ( 1 ) O(1) O(1)

1.一维前缀和优化

题目链接

#include <iostream>
using namespace std;
const int N = 110,M=100010,mod=1e9+7;
int f[N][M];//前i个人且共分了j个蛋糕 
int sum[N][M];//f的前缀和 
int a[N];
int n,m;
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	f[1][0]=sum[1][0]=1;
	for(int i=1;i<=m;i++)f[1][i]=(i<=a[1]),sum[1][i]=f[1][i]+sum[1][i-1];
	for(int i=2;i<=n;i++)
	{
		f[i][0]=sum[i][0]=1;
		for(int j=1;j<=m;j++)
		{
			if(j<=a[i])f[i][j]=sum[i-1][j]%mod;
			else f[i][j]=(sum[i-1][j]-sum[i-1][j-a[i]-1]+mod)%mod;
			sum[i][j]=(sum[i][j-1]+f[i][j])%mod;
		}
	}
	cout<<f[n][m]<<endl;
}

2.二维前缀和优化

题目链接

#include <iostream>
using namespace std;
#define int long long
const int N = 2010,mod = 1e9+7;
char g[N][N];
int f[N][N];
int sum[N][N][3];
signed main()
{
    
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)scanf("%s",g[i]+1);
	f[1][1]=1;
    for(int i=1;i<=n;i++)
    {
    	for(int j=1;j<=m;j++)
    	{
    		if(g[i][j]!='#')
    		{
    			f[i][j]=(f[i][j]+sum[i-1][j][0]+sum[i][j-1][1]+sum[i-1][j-1][2])%mod;
				sum[i][j][0]=(sum[i-1][j][0]+f[i][j])%mod;
    			sum[i][j][1]=(sum[i][j-1][1]+f[i][j])%mod;
    			sum[i][j][2]=(sum[i-1][j-1][2]+f[i][j])%mod;
			}
			else
			{
				sum[i][j][0]=sum[i][j][1]=sum[i][j][2]=0;
			}
		}
	}
	cout<<f[n][m]<<endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_WAWA鱼_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值