线性,背包,区间DP例题

P1282多米诺骨牌

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

状态转移方程:

\[f[i][j]=min(f[i-1][j-a[i]],f[i-1][j-b[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[i-1][j],f[i-1][max(j-p[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[i-1][j][k],if(a[i]!=b[j])\)

\(f[i-1][j][k]+f[i-1][j-1][k-1],if(a[i]==b[j],a[i-1]!=b[j-1])\)

\(f[i-1][j][k]+f[i-1][j-1][k-1]+f[i-1][j-2][k-1],if(a[i]==b[j],a[i-1]==b[j-1],a[i-2]!=b[j-2])\)

......

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

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

\(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\)天卖出......第\(j-1\)天买入,第\(j\)天卖出。这时就可以将当天的价格看作体积,将当天与后一天的价格差当做价值,进行完全背包。

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

\[f[i][j][k]=max(f[i][j][k],f[i][j-1][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:\(A\)\(B\)都不是空格

2:\(A\)是空格,\(B\)不是

3:\(A\)不是空格,\(B\)

\(dp[i][j][0/1/2]\)表示\(A\)\(0\)$i-1$和$B$的$0$\(j-1\)匹配,且最后一位分别是情况\(1,2,3\)时的最优解。

初始化:\(dp[0][i][1]=-a-b*(i-1),dp[0][i][0]=dp[0][i][2]=-1e9\)

\(dp[i][0][2]=-a-b*(i-1),dp[i][0][0]=dp[i][0][1]=-1e9\)

\(dp[0][0][1]=dp[0][0][2]=-1e9\)

状态转移方程:

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

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

\(f[i][j][2]=max(f[i-1][j][0]-a,max(f[i-1][j][1]-a,f[i-1][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\) 位中有 \(k\)\(1\) ,向第 \(i\) 位的下一位进位 \(l\)

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

假设当前序列 \(a\) 中分配了 \(t\) 个元素的值为 \(i\) ,加上上一位进过来的 \(l\)\(1\) ,总共要向下一位进位\(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]\times v^t_i\times C(n-j,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[i-1][j-a[i]][k]\)\(f[i-1][j][k-b[i]]\) , \(f[i-1][j-c[i]][k-c[i]]\) 转移而来。并且不能发现,第一位是可以滚掉的。时间复杂度为 \(O(n^3)\)

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

状态转移方程:

\(f[i][j]=min(f[i][j],f[i-1][j-a[i]],f[i-1][j]+b[i],f[i-1][j-c[i]]+c[i])\)

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

另外,直接写的时间复杂度为 \(O(6000 \times 30000)=O(1.8\times 10^8)\),仍有超时的风险。一个优化的策略是每次循环找到 \(j\) 的上界,而不是一直让 \(j\)\(0\) 循环到 \(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[j-v[i]][1]+w[i]>f[j-v[i]][2]+w[i]>...>f[j-v[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][j-1][0]+(k+1)\times (k+1))\)

第二种情况:区间 \([i,j]\) 内部有和 \(j\) 颜色相同的块,消掉它们中间的块,把它们拼接到一起,\(dp[i][j][k]=max(dp[i][j][k],dp[i][p][k+1]+dp[p+1][j-1][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 @ 2023-03-29 16:41  andy_lz  阅读(30)  评论(0)    收藏  举报