状压DP例题

P2831愤怒的小鸟

首先记录抛物线的方案。根据题意可知,两个点可能会确定一条符合题设的抛物线。所以\(O(n^2)\)枚举两个点,如果它们能够构成一个符合题设的抛物线,就再\(O(n)\)扫一遍,将这个抛物线能够到达的点记录下来,状态压缩记录成一种方案。别忘了只有抛物线只到达一个点也是一种方案。于是我们得到一个数组\(s\)\(s[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\) 个人到第 \(i-1\) 个人已经打完饭,第 \(i\) 个人以及后面七个人是否打饭的状态为 \(j\) ,当前最后一个打饭的人编号为 \(i+k\) .

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

如果第 \(i\) 个人还没有打好饭,我们可以枚举 \(i\)\(i+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[i-1]\) ,则 \(b[i]=b[i-1]\),否则 \(b[i]=b[i-1]+a[i]-a[i-1]\)

\(f[i][j][k]\) 表示已经选的状态为 \(i\) ,最后一个选的 \(a[j]\) ,当前 \(b\) 已经分配的总和为 \(k\) 。转移时考虑费用提前计算,即另后面尚未选的所有\(a\)都分配上当前的分配值。状态转移方程:\(f[i][j][k]+=f[i-(1<<(x-1))][y][k-(a[y]-a[x])\times (n-cnt1(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[i-1][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[i-2^{j-1}][k]+dis(j,k))\)

那么加上空间限制怎么办呢?可以考虑滚动数组。设\(f[i\&1][j][t]\)表示当前状态的二进制有\(i\)\(1\),状态的映射为\(j\),最后一个停留的点为\(t\)的最短距离。我们状态的二进制最多有\(20\)位,当其中\(1\)的数量相同时,最多有\(C_{20}^{10}=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\) 的最小代价。状态转移方程:

\[\begin{cases}f[i][j][k][l]=min\{f[i][j][k][l],f[i-1][j-1][k][l]\}\\f[i][j][k|2^{a[i]-1}][a[i]]=min\{f[i][j][k|2^{a[i]-1}][a[i]],f[i-1][j][k][l]\} \end{cases} \]

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

posted @ 2023-03-29 16:43  andy_lz  阅读(43)  评论(0)    收藏  举报