线性,背包,区间DP例题

P1282多米诺骨牌

容易发现一个性质:对于前i个牌子,它们的点数总和加起来是一个定值。所以,设f[i][j]表示前i个多米诺骨牌的第一行的和为j时的最小旋转次数。

状态转移方程:

f[i][j]=min(f[i1][ja[i]],f[i1][jb[i]]+1))

初始化:

f[1][a[1]]=0;f[1][b[1]]=1;其余全部是正无穷

code:

int k(int x){
	return x>0?x:-x;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d%d",&a[i],&b[i]);
		sum+=a[i]+b[i];
	}
	for(int i=1;i<=n;++i)
		for(int j=0;j<=sum;++j)
			f[i][j]=1e9;
	f[1][a[1]]=0;f[1][b[1]]=1;
	for(int i=2;i<=n;++i)
		for(int j=0;j<=sum;++j){
			f[i][j]=1e9;
			if(j>=a[i])
				f[i][j]=min(f[i][j],f[i-1][j-a[i]]);
			if(j>=b[i])
				f[i][j]=min(f[i][j],f[i-1][j-b[i]]+1);
		}
	minn=1e9;
	for(int i=0;i<=sum;++i)
		if(f[n][i]!=1e9&&k(sum-i-i)<minn)
			minn=k(sum-i*2),ans=f[n][i];
		else if(f[n][i]!=1e9&&k(sum-i-i)==minn)
			ans=min(ans,f[n][i]);
	printf("%d\n",ans);
	return 0;
}

P4138挂饰

f[i][j]表示前i个物品,剩余
至少j个空挂钩时的最大喜悦值。

状态转移方程:

f[i][j]=min(f[i1][j],f[i1][max(jp[i].a,0)+1]+p[i].b)

初始化:f[0][1]=0,其余全部是负无穷

需要注意的是,在DP前要按照挂钩的数量从大到小排序,因为排序之后才能使挂钩数量尽可能多,越有可能使得结果更优。如果不排序,可能很快就没有了挂钩,错过了后面权值特别大的点。

code:

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d%d",&p[i].a,&p[i].b);
	sort(p+1,p+n+1,cmp);
	for(int i=0;i<=n;++i)
		for(int j=0;j<=n+1;++j)
			f[i][j]=-1e9;
	f[0][1]=0;
	for(int i=1;i<=n;++i){
		for(int j=n;j>=0;--j){
			f[i][j]=max(f[i-1][j],f[i-1][max(j-p[i].a,0)+1]+p[i].b);
		}
	}
	for(int i=0;i<=n;++i)
		ans=max(ans,f[n][i]);
	printf("%d\n",ans);
	return 0;
}

P2679子串

f[i][j][k]表示从A串的前i个字符中取k个不重复子串,拼接起来后与B的前j个子串相等的方案数量。

状态转移方程:

f[i][j][k]=

f[i1][j][k],if(a[i]!=b[j])

f[i1][j][k]+f[i1][j1][k1],if(a[i]==b[j],a[i1]!=b[j1])

f[i1][j][k]+f[i1][j1][k1]+f[i1][j2][k1],if(a[i]==b[j],a[i1]==b[j1],a[i2]!=b[j2])

......

如果直接算,时间复杂度是O(nm2k),明显超时。这时我们可以用前缀和来优化DP,时间复杂度会降至O(nmk)

另外,此时的空间复杂度为1000×200×200×2(开long long),也就是 8e7 ,也会爆炸。观察式子,我们可以发现每一次转移都是f[i1][...]转移到f[i][...],所以我们可以直接滚掉第一维,将空间复杂度优化到 200×200×2

code:

int main(){
	cin>>n>>m>>k;
	cin>>a>>b;
	sum[0][0]=1;
	for(ll i=1;i<=n;++i){
		for(ll j=min(i,m);j>=1;--j){
			for(ll p=min(j,k);p>=1;--p){
				if(a[i-1]==b[j-1])
					f[j][p]=(f[j-1][p]+sum[j-1][p-1])%mod; 
				else
					f[j][p]=0;
				sum[j][p]=(sum[j][p]+f[j][p])%mod;//sum[i][j][l]维护f[1][j][l]~f[i][j][l]的前缀和
			}
		}
	}
	cout<<sum[m][k]%mod<<endl;
	return 0;
}

P5662 [CSP-J2019] 纪念品

题目中有一句关键的话“每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。”我们可以根据这句话简化问题:把第i天买入,第j天卖出看作:第i天买入,第i+1天卖出,第i+1天买入,第i+2天卖出......第j1天买入,第j天卖出。这时就可以将当天的价格看作体积,将当天与后一天的价格差当做价值,进行完全背包。

f[i][j][k] 表示第i天,前j个物品,手中的钱数为k时候的最大收益。状态转移方程:

f[i][j][k]=max(f[i][j][k],f[i][j1][k+p[i][j]]+p[i+1][j]p[i][j])

考虑滚动数组,我们发现只需要保留k这一维即可。

code:

int main(){
	cin>>t>>n>>m;
	for(int i=1;i<=t;++i)
		for(int j=1;j<=n;++j)
			cin>>a[i][j];
	ans=m;
	for(int i=1;i<=t-1;++i){
		for(int l=0;l<=ans;++l)
			dp[l]=0;
		for(int j=1;j<=n;++j){
			for(int k=a[i][j];k<=ans;++k){
				dp[k]=max(dp[k],dp[k-a[i][j]]-a[i][j]+a[i+1][j]);
			}
		}
		ans+=dp[ans];
	}
	cout<<ans<<endl;
	fclose(stdin);fclose(stdout);
	return 0;
}

P4059找爸爸

首先要知道,两个序列的某一个位置都是空格的情况肯定不是最优解。因为题目中k的系数是负数,k增加会令答案更劣。所以对于一个位置只考虑三种情况:

1:AB都不是空格

2:A是空格,B不是

3:A不是空格,B

dp[i][j][0/1/2]表示A0i1B0j1匹配,且最后一位分别是情况1,2,3时的最优解。

初始化:dp[0][i][1]=ab(i1),dp[0][i][0]=dp[0][i][2]=1e9

dp[i][0][2]=ab(i1),dp[i][0][0]=dp[i][0][1]=1e9

dp[0][0][1]=dp[0][0][2]=1e9

状态转移方程:

f[i][j][0]=max(f[i1][j1][0],max(f[i1][j1][1],f[i1][j1][2]))+d[x[i1]][y[j1]];

f[i][j][1]=max(f[i][j1][0]a,max(f[i][j1][1]b,f[i][j1][2]a));

f[i][j][2]=max(f[i1][j][0]a,max(f[i1][j][1]a,f[i1][j][2]b));

code:

int main(){
    cin>>x>>y;
    cin>>d['A']['A']>>d['A']['T']>>d['A']['G']>>d['A']['C'];
    cin>>d['T']['A']>>d['T']['T']>>d['T']['G']>>d['T']['C'];
    cin>>d['G']['A']>>d['G']['T']>>d['G']['G']>>d['G']['C'];
    cin>>d['C']['A']>>d['C']['T']>>d['C']['G']>>d['C']['C'];
    cin>>a>>b;
    for(int i=0;i<y.size();++i){
        f[0][i+1][1]=-a-b*i;
        f[0][i+1][2]=f[0][i+1][0]=-1e9;
    }
    for(int i=0;i<x.size();++i){
        f[i+1][0][2]=-a-b*i;
        f[i+1][0][1]=f[i+1][0][0]=-1e9;
    }
    f[0][0][1]=f[0][0][2]=-1e9;
    for(int i=0;i<x.size();++i)
        for(int j=0;j<y.size();++j){
            f[i+1][j+1][0]=max(f[i][j][0],max(f[i][j][1],f[i][j][2]))+d[x[i]][y[j]];
            f[i+1][j+1][1]=max(f[i+1][j][0]-a,max(f[i+1][j][1]-b,f[i+1][j][2]-a));
            f[i+1][j+1][2]=max(f[i][j+1][0]-a,max(f[i][j+1][1]-a,f[i][j+1][2]-b));
        }
    printf("%d\n",max(f[x.size()][y.size()][0],max(f[x.size()][y.size()][1],f[x.size()][y.size()][2])));
    return 0;
}

P7961 [NOIP2021]数列

考虑按照 S 的二进制位数进行 DP

f[i][j][k][l] 表示当前是 S 从低到高的第 i 位,已经确定了 j 个序列 a 中的元素, S 从低到高前 i 位中有 k1 ,向第 i 位的下一位进位 l

如果从前面转移到 f[i][j][k][l] ,细节太多,不好写。所以考虑从 f[i][j][k][l] 往后转移。

假设当前序列 a 中分配了 t 个元素的值为 i ,加上上一位进过来的 l1 ,总共要向下一位进位t+l,下一位进位的时候要进位 (t+l)/2 .

f[i][j][k][l] 往后要转移的状态是: f[i+1][j+t][k+(t+l) mod 2][(l+p)/2]

状态转移方程:

f[i+1][j+t][k+(t+l) mod 2][(l+p)/2]

=f[i][j][k][l]×vit×C(nj,t)

code:

long long bit1(long long x){
	long long cnt=0;
	while(x){
		x-=x&(-x);
		++cnt;
	} 
	return cnt;
}
int main(){
	scanf("%lld%lld%lld",&n,&m,&p);
	for(int i=0;i<=m;++i){
		scanf("%lld",&v[i]);
		pv[i][0]=1;
		for(int j=1;j<=n;++j)
			pv[i][j]=pv[i][j-1]*v[i]%mod;
	}
	for(int i=0;i<=100;++i)
		c[i][0]=1;
	for(int i=1;i<=100;++i)
		for(int j=1;j<=100;++j)
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
	f[0][0][0][0]=1;
	for(int i=0;i<=m;++i)
		for(int j=0;j<=n;++j)
			for(int k=0;k<=p;++k)
				for(int l=0;l<=(n>>1);++l)
					for(int t=0;t<=n-j;++t)
						f[i+1][j+t][k+((t+l)&1)][(l+t)>>1]=(f[i+1][j+t][k+((t+l)&1)][(l+t)>>1]+f[i][j][k][l]*c[n-j][t]%mod*pv[i][t]%mod)%mod;
	for(int i=0;i<=p;++i)
		for(int j=0;j<=(n>>1);++j)
			if(i+bit1(j)<=p)
				ans=(ans+f[m+1][n][i][j])%mod;
	printf("%lld\n",ans);
	return 0;
}

P2224产品加工

看到这道题,不难想到暴力DP:设 f[i][j][k] 表示前 i 个数,用机器 A 的时间和为 j ,用机器 B 的时间和为 k 的状态是否存在。 f[i][j][k] 可以由 f[i1][ja[i]][k]f[i1][j][kb[i]] , f[i1][jc[i]][kc[i]] 转移而来。并且不能发现,第一位是可以滚掉的。时间复杂度为 O(n3)

那么怎么优化呢?这道题采用了一种很新颖的方法:将答案设为状态。设 f[i][j] 表示前 i 个物品,用 A 做的时间为 j 时, B 做的时间最小值。

状态转移方程:

f[i][j]=min(f[i][j],f[i1][ja[i]],f[i1][j]+b[i],f[i1][jc[i]]+c[i])

不难发现,在倒叙枚举 j 的情况下,第一维能够直接滚掉。

另外,直接写的时间复杂度为 O(6000×30000)=O(1.8×108),仍有超时的风险。一个优化的策略是每次循环找到 j 的上界,而不是一直让 j0 循环到 30000

code:

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        scanf("%d%d%d",&a[i],&b[i],&c[i]);
    ans=1e9;
    for(int i=1;i<=30000;++i)
        f[i]=1e9;
    for(int i=1;i<=n;++i){
        up+=max(a[i],c[i]);
        for(int j=up;j>=0;--j){
            int x,y,z;x=y=z=1e9;
            if(a[i]&&j>=a[i])
                x=f[j-a[i]];
            if(b[i])
                y=f[j]+b[i];
            if(c[i]&&j>=c[i])
                z=f[j-c[i]]+c[i];
            f[j]=min(x,min(y,z));
            if(i==n)
                ans=min(ans,max(j,f[j]));
        }
    }
    printf("%d\n",ans);
    return 0;
}

P1858多人背包

f[j][l] 表示体积为 j ,当前为第 l 优解的价值。

接下来考虑转移。首先外层枚举每个物品 i ,内层从大到小枚举体积 j 。然后我们发现, f[j][1]>f[j][2]>f[j][3]>...>f[j][k] ,且 f[jv[i]][1]+w[i]>f[jv[i]][2]+w[i]>...>f[jv[i]][k]+w[i] 。所以我们就可以用归并排序的思想,找出其中最大的 k 个数,来依次更新 f[j][1],f[j][2]...f[j][k]

code:

for(int i=1;i<=n;++i){
	for(int j=v;j>=a[i];--j){
		int k1=1,k2=1,cnt=0;
		for(int l=1;l<=k;++l)
			now[j][l]=f[j][l];
		while(cnt<=k){
			if(f[j][k1]>f[j-a[i]][k2]+b[i]){
				now[j][++cnt]=f[j][k1];
				++k1;
			}
			else{
				now[j][++cnt]=f[j-a[i]][k2]+b[i];
				++k2;
			}
		}
		for(int l=1;l<=k;++l)
			f[j][l]=now[j][l];
	}
}

方块消除 Blocks

经典的区间消除问题。

传统的想法是,设 dp[i][j] 表示消去区间 [i,j] 的最小分数。然而,这样无法考虑到区间内部和区间外部的块一起消除的情况,无法满足最优子结构,并且不好转移,所以考虑重新设计状态。

因为内部的块会受到外部的块的影响,所以设 dp[i][j][k] 表示消除区间 [i,j] 以及区间 [i,j] 后面 k 个与 j 颜色相同的块的最大分数。

首先考虑第一种情况: k 个块和第 j 个块一起消除:dp[i][j][k]=max(dp[i][j][k],dp[i][j1][0]+(k+1)×(k+1))

第二种情况:区间 [i,j] 内部有和 j 颜色相同的块,消掉它们中间的块,把它们拼接到一起,dp[i][j][k]=max(dp[i][j][k],dp[i][p][k+1]+dp[p+1][j1][0])

code:

int dfs(int l,int r,int k){
	if(l>r)
		return 0;
	if(dp[l][r][k])
		return dp[l][r][k];
	dp[l][r][k]=max(dp[l][r][k],dfs(l,r-1,0)+(k+1)*(k+1));
	for(int p=nxt[r];p>=l;p=nxt[p])
		dp[l][r][k]=max(dp[l][r][k],dfs(l,p,k+1)+dfs(p+1,r-1,0));
	return dp[l][r][k];
}
signed main(){
	scanf("%lld",&t);
	while(t--){
		scanf("%lld",&n);
		for(int i=1;i<=n;++i)
			scanf("%lld",&a[i]),nxt[i]=0;
		for(int i=1;i<=n;++i)
			for(int j=i-1;j>=1;--j)
				if(a[j]==a[i])
					{nxt[i]=j;break;}
		for(int i=1;i<=n;++i)
			for(int j=i;j<=n;++j)
				for(int k=0;k<=n;++k)
					dp[i][j][k]=0;
		printf("Case %lld: %lld\n",++cnt,dfs(1,n,0));
	}
	return 0;
}

三倍经验:CF1107E P5336

posted @   andy_lz  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示