动态规划优化技巧

浅谈动态规划基础优化

原题:https://www.luogu.com.cn/problem/P1776

这题虽然标绿,但是数据极水,通过解绑优化即可卡着1s时限通过
未优化代码:

const int N=1e5+5;
int v[N],w[N],m[N];
int dp[N];
void solve(){
    int n,W;cin>>n>>W;

    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i]>>m[i];
    }

    for(int i=1;i<=n;i++){
        for(int j=W;j>=w[i];j--){
            for(int k=1;k<=m[i] and k*w[i]<=j;k++){
                dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
            }
        }
    }

    cout<<dp[W];
}

优化方法1:二进制拆分

const int N=1e5+5;
int v[N],w[N],m[N];
int tv[N],tw[N];
int dp[N];
void solve(){
    int n,W;cin>>n>>W;

    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i]>>m[i];
    }
    //二进制拆分
    int idx=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m[i];j<<=1){
            m[i]-=j; //减去已拆分的
            //定义二进制意义下的新物品
            tw[++idx]=j*w[i];
            tv[idx]=j*v[i];
        }
        // 若有余数也要加进来
        if(m[i]){
            tw[++idx]=m[i]*w[i];
            tv[idx]=m[i]*v[i];
        }
    }
    //用新物品进行01背包
    for(int i=1;i<=idx;i++){
        for(int j=W;j>=tw[i];j--){
            dp[j]=max(dp[j],dp[j-tw[i]]+tv[i]);
        }
    }
    cout<<dp[W];
}

再看一道Baka题,https://www.luogu.com.cn/problem/P1725
从题意可得,Baka的每一步是在\([L+i,R+i]\)中选择,不难想到应当取该区间内的最大值,朴素dp想法的时间复杂度为\(O(n^2)\)

for(int i=1;i<=(n+r-1);i++){ //最多只能跳到n+r-1的位置
    for(int j=max(0,i-r);j<=(i-l);j++){ //暴力枚举区间最大值
        dp[i]=max(dp[i],dp[j]+a[i]);
        if(i>=n) ans=max(ans,dp[i]); //超出范围则统计答案
    }
}

优化方法2:单调队列&&优先队列

由于是对区间求最大值,可以采用单调队列的方式进行优化,由于在队列中每一个元素仅进出一次,因此时间复杂度可以降至\(O(n)\),因此对于Baka题的参考代码如下

const int N=1e6+5;
int a[N],dp[N],q[N];//q为手写队列,写起来比stl的双端队列方便
void solve(){
    int n,l,r;cin>>n>>l>>r;
    for(int i=0;i<=n;i++){
        cin>>a[i];
    }
    int ans=-1e9;
    memset(dp,0xcf,sizeof dp);//先将dp数组的值初始化为无穷小
    int head=1,tal=1;
    int pos=0;
    dp[0]=0;//初始化开始位置
    for(int i=l;i<=n;i++){
        while(head<=tal && dp[q[tal]]<dp[pos]) tal--;
        q[++tal]=pos;
        while(q[head]+r<i) head++;
        dp[i]=dp[q[head]]+a[i];
        pos++;
    }

    for(int i=n+1-r;i<=n;i++){
        ans=max(ans,dp[i]);
    }
    cout<<ans<<endl;
}

再看一题,涉及到对dp数组空间复杂度的优化,例如:https://www.luogu.com.cn/problem/P11998
虽然是一道概率dp题,但是转移方程非常显然,朴素想法是定义\(dp_{i,j}\),其\(i\)表示第\(i\)的物品的概率(模意义下),\(j\)则表示前\(j\)个数模\(m\)意义下的值,即\(dp_{i,j}=(1-p_i)*dp_{i-1,j}+p_i*dp_{i-1,j-a_i modm}\)
但是注意到,本题\(n\)的数据范围去到了\(10^5\),若采用二维数组是无法在规定的内存大小内解决题目的
因此:

优化方法3:滚动数组

滚动数组的前提是:在不影响其他状态转移的前提下,进行状态值的覆盖,从而复用了数组空间。
如何确保在转移的时候不覆盖掉仍有作用的状态值?答案是可以利用一个新的数组进行临时存储,dp完毕后再进行更新,因此本题的参考代码为:

const int mod=998244853;//注意这个不寻常的模数
void solve(){
	int n,m;cin>>n>>m;
	vector<ll> a(n),p(n);
	for(int i=0;i<n;i++){
		cin>>a[i];
		a[i]%=m;//确保a[i]是模意义下的值
	}
	for(int i=0;i<n;i++){
		cin>>p[i];
	}
	vector<ll> f(m,0);
	f[0]=1;
	for(int i=0;i<n;i++){
		vector<ll> t(m,0);
		for(int j=0;j<m;j++){
			t[j]=((1-p[i]+mod)%mod*(f[j])%mod)+(p[i]%mod*f[(j-a[i]+m)%m]%mod);//注意(1-pi)的正负
			t[j]%=mod;
		}
		f=move(t);
	}
	cout<<f[0]<<endl;
}

posted on 2024-11-14 20:50  TaopiTTT  阅读(47)  评论(0)    收藏  举报