状压DP例题

P2831愤怒的小鸟

首先记录抛物线的方案。根据题意可知,两个点可能会确定一条符合题设的抛物线。所以O(n2)枚举两个点,如果它们能够构成一个符合题设的抛物线,就再O(n)扫一遍,将这个抛物线能够到达的点记录下来,状态压缩记录成一种方案。别忘了只有抛物线只到达一个点也是一种方案。于是我们得到一个数组ss[i]表示第i种方案的状态。

接下来是状压DP。设f[i]表示状态为i时的最小抛物线数量。枚举s中的每一个元素,另f[i|s[j]]=min(f[i|s[j]],f[i]+1),答案是f[(1<<n)1]

code:

bool function(double x,double y){
	double fx=a*x*x+b*x-y;
	fx=fx>0?fx:-fx;
	return fx<=exp;
}
void sol(){
	for(int i=1;i<=n;++i)
		for(int j=i+1;j<=n;++j){
			if(p[i].x!=p[j].x){
				a=(p[i].x/p[j].x*p[j].y-p[i].y)/((p[j].x-p[i].x)*p[i].x);
				b=(p[i].y-p[i].x*p[i].x*a)/p[i].x;
				if(a>=0)
					continue;
				s[++tot]|=(1<<(i-1));s[tot]|=(1<<(j-1));
				for(int k=1;k<=n;++k)
					if(k!=i&&k!=j&&function(p[k].x,p[k].y))
						s[tot]|=(1<<(k-1));
			}
		}
	for(int i=1;i<=n;++i)
		s[++tot]|=(1<<(i-1));
}
int main(){
	scanf("%d",&t);
	while(t--){
		for(int i=1;i<=tot;++i)
			s[i]=0;
		tot=0;
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;++i)
			scanf("%lf%lf",&p[i].x,&p[i].y);
		sol();
		for(int i=0;i<(1<<n);++i)
			f[i]=1e9;
		f[0]=0;
		for(int i=0;i<(1<<n);++i){
			for(int j=1;j<=tot;++j){
				f[i|s[j]]=min(f[i|s[j]],f[i]+1);
			}
		}
		//for(int i=1;i<=(1<<n);++i)cout<<f[i]<<" ";
		printf("%d\n",f[(1<<n)-1]);
	}
	return 0;
}

P2157学校食堂

f[i][j][k] 表示第 1 个人到第 i1 个人已经打完饭,第 i 个人以及后面七个人是否打饭的状态为 j ,当前最后一个打饭的人编号为 i+k .

如果第 i 个人打好了饭(即 j&1==true ),则状态转移方程为 f[i+1][j>>1][k1]=min(f[i+1][j>>1][k1],f[i][j][k]).

如果第 i 个人还没有打好饭,我们可以枚举 ii+7 的所有人,让他们先打饭。也就是枚举 h=0...7 ,状态转移方程为 f[i][j|(1<<h)][h]=min(f[i][j|(1<<h)][h],f[i][j][k]+time(i+k,i+h)).

需要注意的是 k 的范围是 [8,0] ,写代码的时候要将 k 整体加上 8.

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

答案: f[n+1][0][k](k[0,8])

void solve(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d%d",&t[i],&b[i]);
	for(int i=0;i<=n+1;++i)
		for(int j=0;j<=(1<<8);++j)
			for(int k=-8;k<=7;++k)
				f[i][j][k+8]=1e9;
	f[1][0][7]=0;
	for(int i=1;i<=n;++i)
		for(int j=0;j<(1<<8);++j)
			for(int k=-8;k<=7;++k)
				if(f[i][j][k+8]!=1e9){
					if(j&1)
						f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);
					else{
						int tmp=1e9;
						for(int h=0;h<=7;++h)
							if(!((j>>h)&1)){
								if(i+h>tmp)
									break;
								tmp=min(tmp,i+h+b[i+h]);
								f[i][j|(1<<h)][h+8]=min(f[i][j|(1<<h)][h+8],f[i][j][k+8]+(i+k?(t[i+k]^t[i+h]):0));
							}
					}
				}
	ans=1e9;
	for(int k=0;k<=8;++k)
		ans=min(ans,f[n+1][0][k]);
	printf("%d\n",ans);
	return;
}

P7519 [省选联考 2021 A/B 卷] 滚榜

首先想到,答案是最终排名的方案数,与 b 的方案数无关。所以考虑每次使 b 尽量少得分配给当前的数 a

a[i]>a[i1] ,则 b[i]=b[i1],否则 b[i]=b[i1]+a[i]a[i1]

f[i][j][k] 表示已经选的状态为 i ,最后一个选的 a[j] ,当前 b 已经分配的总和为 k 。转移时考虑费用提前计算,即另后面尚未选的所有a都分配上当前的分配值。状态转移方程:f[i][j][k]+=f[i(1<<(x1))][y][k(a[y]a[x])×(ncnt1(i)+1)]

代码实现时注意分数相等时,位置小的靠前。

code:

int main(){
	scanf("%lld%lld",&n,&m);
	a[0]=-1;
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
		if(a[maxn]<a[i])
			maxn=i;
	}
	for(int i=1;i<=n;++i){//一开始的节点要成为第一,所以要比最大值大 
		long long sum=n*(a[maxn]-a[i]);
		if(maxn<i)
			sum+=n;
		if(sum<=m)
			f[1<<(i-1)][i][sum]=1;
	}
	for(int i=0;i<(1<<n);++i){
		int tmp=i,cnt=0,num=1;
		while(tmp){
			if(tmp&1) b[++cnt]=num;
			++num;tmp>>=1;
		}
		for(int j=1;j<=cnt;++j){
			int x=b[j];
			if(i&(1<<(x-1))){
				for(int k=1;k<=cnt;++k){
					int y=b[k];
					if(j!=k&&(i&(1<<(y-1)))){
						int sum=(a[y]-a[x])*(n-cnt+1);
						if(y<x) sum+=n-cnt+1;
						if(a[y]<a[x]) sum=0;
						for(int l=m;l>=sum;--l)
							f[i][x][l]+=f[i-(1<<(x-1))][y][l-sum];//cout<<f[i][x][l]<<" "<<i-(1<<(x-1))<<" "<<y<<" "<<l-sum<<endl;
					}
				}
			}
		}
	}
	for(int i=1;i<=n;++i)
		for(int j=0;j<=m;++j)
			ans+=f[(1<<n)-1][i][j];
	printf("%lld\n",ans);
	return 0;
}

P2150 [NOI2015] 寿司晚宴

因为两个人选的数字全部互质,所以甲选的数字的质因数集合和乙选的数字的质因数集合没有交集

dp[s1][s2] 表示甲选的数字的质因数集合是 s1 ,乙选择的数字的质因数集合是 s2 的方案数。

状态转移方程: dp[i][s1|k][s2]+=dp[i1][s1][s2](k&s2==0) ,其中, k 是当前的质因数集合。

然而, 500 以内的质因数有很多,直接状压肯定超时。所以需要考虑优化。

我们发现,一个小于500的数字,最多只有一个比22大的质因数。所以考虑单独计算这个含有这个大质因数的数的贡献。

f1[s1][s2],f2[s1][s2] 分别表示这个数让甲选,让乙选的方案数。首先将所有数按照最大质因数的大小从小到大排序,然后对于所有最大质因数相同的数,先将 dp 数组的数值赋予 f1 , f2 ,然后推出 f1 , f2 ,最后将 f1 , f2 合并给 dp 数组。

dp[s1][s2]=f1[s1][s2]+f2[s1][s2]dp[s1][s2]

注意这里要减去 dp[s1][s2] ,因为 f1 , f2 会重复统计两个人都不选的情况。

code:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
long long n,m,tot,p[1005],dp[1005][1005],f2[1005][1005],f1[1005][1005],ans;
struct node{
	int w,big;
} a[1005];
bool ok[1005];
bool cmp(node a,node b){
	return a.big<b.big;
}
int main(){
	scanf("%lld%lld",&n,&m);
	const long long mod=m;
	for(int i=2;i<=n;++i)
		if(!ok[i]){
			p[++tot]=i;
			for(int j=i*2;j<=n;j+=i)
				ok[j]=1;
		}
	for(int i=1;i<n;++i){
		for(int j=1;j<=tot;++j){
			if(p[j]<=22&&(i+1)%p[j]==0)
				a[i].w|=1<<(j-1);
			if(p[j]>22&&(i+1)%p[j]==0)
				a[i].big=p[j];
		}
	}
	sort(a+1,a+n,cmp);
	dp[0][0]=1;
	for(int i=1;i<n;++i){
		if(a[i].big==0||i==n-1||a[i].big!=a[i-1].big){
			memcpy(f1,dp,sizeof(f1));
			memcpy(f2,dp,sizeof(f2));
		}
		for(int j=255;j>=0;--j)
			for(int k=255;k>=0;--k)
				if((j&k)==0){
					if((a[i].w&j)==0)
						f2[j][a[i].w|k]=(f2[j][a[i].w|k]+f2[j][k])%mod;
					if((a[i].w&k)==0)
						f1[j|a[i].w][k]=(f1[j|a[i].w][k]+f1[j][k])%mod;
				}
		if(i==n-1||a[i].big!=a[i+1].big||a[i].big==0){
			for(int j=0;j<=255;++j)
				for(int k=0;k<=255;++k)
					if((j&k)==0)
						dp[j][k]=((f1[j][k]+f2[j][k])-dp[j][k]+mod)%mod;
		}
	}
	for(int i=0;i<=255;++i)
		for(int j=0;j<=255;++j)
			ans=(ans+dp[i][j])%mod;
	printf("%lld\n",ans);
	return 0;
}

P3451 [POI2007] ATR-Tourist Attractions

如果没有64MB的限制,那么这就成了一个状压水题。设f[i][j]表示k个点是否停留的状态为i,最后一个停留的点为j的最短距离。状态转移方程:f[i][j]=min(f[i][j],f[i2j1][k]+dis(j,k))

那么加上空间限制怎么办呢?可以考虑滚动数组。设f[i&1][j][t]表示当前状态的二进制有i1,状态的映射为j,最后一个停留的点为t的最短距离。我们状态的二进制最多有20位,当其中1的数量相同时,最多有C2010=184756种不同的数。因此,我们可以将二进制中1的数量相同的数映射到 [1,2e5] 以内。所以开数组时可以开成 f[2][200005][22] ,不会超空间。

void pre_work(){
    for(int i=1;i<=k+2;++i)
        dij(i);//求i到所有点的最短路(特别地,k+2表示n)
    for(int i=0;i<(1<<k);++i){
        int tmp=i,len=0;
        while(tmp){
            if(tmp&1) ++len;
            tmp>>=1;
        }
        sta[++cnt[len]][len]=i;
        sta2[i]=cnt[len];
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;++i){
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w);add(v,u,w);
    }
    scanf("%d",&g);
    for(int i=1;i<=g;++i){
        scanf("%d%d",&u,&v);
        ++in[v];add2(u,v);
    }
    pre_work();
    for(int i=0;i<k;++i){
        clear(i+1);//清空数组
        for(int j=1;j<=cnt[i];++j){
            int tmp=sta[j][i],len=0,num=1;
            for(int t=2;t<=k+1;++t)
                in2[t]=in[t],vis[t]=0;
            while(tmp){
                if(tmp&1)
                    b[++len]=num+1,vis[num+1]=1;
                tmp>>=1;++num;
            }
            if(!sta[j][i])
                b[len=1]=1;
            for(int t=1;t<=len;++t)
                for(int p=head2[b[t]];p;p=nxt2[p])
                    --in2[ver2[p]];
            for(int t=2;t<=k+1;++t)
                if(in2[t]==0&&!vis[t]){
                    int y=sta2[sta[j][i]+(1<<(t-2))];
                    for(int p=1;p<=len;++p)
                        f[(i+1)&1][y][t]=min(f[(i+1)&1][y][t],f[i&1][j][b[p]]+dis[b[p]][t]);
                }
        }
    }
    ans=inf;
    for(int i=1;i<=k+1;++i)
        ans=min(ans,f[k&1][1][i]+dis[k+2][i]);//别忘了最后走到n
    printf("%d\n",ans);
    return 0;
}

P1777 帮助

因为序列的值域很小,所以考虑状压DP。

f[i][j][k][l] 表示前 i 本书,已经选择了 j 本,之前存书集合为 l ,最后一本没取的书编号为 t 的最小代价。状态转移方程:

{f[i][j][k][l]=min{f[i][j][k][l],f[i1][j1][k][l]}f[i][j][k|2a[i]1][a[i]]=min{f[i][j][k|2a[i]1][a[i]],f[i1][j][k][l]}

最后答案是 min{f[n][j][k][l]+count(l xor sta)}count 表示二进制中一的个数, sta 表示所有书构成的二进制状态。

posted @   andy_lz  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示