Luogu P3959 宝藏 题解 [ 紫 ] [ 状压 dp ] [ 二项式定理 ]

宝藏:一个对着蓝书代码调都能调两个小时的大毒瘤,但是思路还是很值得借鉴的,有普通状压和三进制状压两种做法,或者暴搜剪枝也可以(这里不介绍暴搜剪枝做法)。

普通状压做法

观察到 n12,首先想到状压。

但考虑到普通的状压不太行,因为 K 这个数算在代价里,会导致这个 dp 有后效性。同时也观察到最终形成的方案一定是一棵树

因此,我们尝试把 K 加入状态中。

定义 dpK,i 表示这棵树扩展到第 K 层,状态为 i 时的最小花费。

那么我们在转移的时候枚举一下 i 的子集 j,从 dpK1,j+cost×(K1) 转移就好了。

所以我们预处理的时候要把每个状态的能转移到该状态的子集列出来。

这个操作,我们可以先列出每个状态拓展所有点的边后的状态 expdi,并且记录下每个状态 i 扩展第 j 个点的最小花费 roadi,j,目的是便于计算 cost 的值。
于是我们枚举每一个状态的子集,判断这个状态是否是该子集的 expd 的子集。如果是,则可以转移,枚举所有需要扩展的点,加上它的 road 即可。

细节

枚举某个状态的子集

枚举 i 这个状态的非零子集,可以通过如下代码实现:

for(int j=i;j!=0;j=((j-1)&i))

如果要枚举 0,必须特判;如果不能枚举自身,那么把 j 初始化重设一下:int j=((i-1)&i)

例子:枚举 14 的非零子集(包括自己)。

image

因为每次都从某个子集减 1,并且还与了自身,所以保证每次都是子集,且比以前都小,保证了不重、不漏。

位运算

注意优先级,很容易被坑,多打括号。

优先级从大到小:

  1. * 和 / 乘除
  2. + 和 - 加减
  3. >> 和 << 左移右移
  4. > 和 < 和 == 和 != 和 >= 和 <= 比较符
  5. ^ (xor) 异或
  6. | 位或

注意: ! 和 ~ 的优先级很高,不要乱用,甚至高于加减的优先级,尽量加括号使用!!!

时间复杂度分析

二项式定理:

(a+b)n=Cn0anb0+Cn1an1b1++Cnkankbk++Cnna0bn

其中,组合数的系数可以看作是杨辉三角里的数,其实这个定理初二就学过。

该定理可以逆用。

那么我们用此来解决本题枚举子集部分的复杂度分析:

2n+Cn12n1+Cn22n2+...+1=(2+1)n=3n

这就是 dp 部分复杂度,预处理的复杂度为 O(m×2n)

总体复杂度 O(n×3n+m×2n)

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
int n,m,expd[4505];
ll d[15][15],road[4505][15],dp[15][4505],ans=0x3f3f3f3f3f3f3f3f;
vector<pi>frm[4505];
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	memset(d,0x3f,sizeof(d));
	memset(road,0x3f,sizeof(road));
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		d[u][v]=d[v][u]=min(1ll*w,d[u][v]);
	}
	for(int i=1;i<=n;i++)d[i][i]=0;
	//初始化expand和road函数
	for(int i=0;i<(1<<n);i++)
	{
		expd[i]=i;
		for(int j=1;j<=n;j++)
		{
			if(((i>>(j-1))&1)==0)continue;//位运算不要偷懒,这里写 !(i>>(j-1))&1 是错的!
			road[i][j]=0;
			for(int k=1;k<=n;k++)
			{
				if(((i>>(k-1))&1)==1)continue;
				if(d[j][k]>=(0x3f3f3f3f/2))continue;
				expd[i]=expd[i]|(1<<(k-1));
				road[i][k]=min(road[i][k],d[j][k]);
			}
		}
	}
	//初始化每个状态的子集们
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=i;j!=0;j=((j-1)&i))
		{
			if((i&expd[j])!=i)continue;
			ll cst=0;
			for(int k=1;k<=n;k++)
			{
				if(((i^j)>>(k-1))&1)cst+=road[j][k];
			}
			frm[i].push_back({j,cst});
		}
	}
	//状压dp
	memset(dp,0x3f,sizeof(dp));
	for(int i=0;i<n;i++)dp[1][1<<i]=0;
	for(int i=1;i<=n;i++)//层数
	{
		for(int j=0;j<(1<<n);j++)//当前状态
		{
			for(auto tmp:frm[j])
			{
				int st=tmp.first;
				ll cst=tmp.second;
				dp[i][j]=min(dp[i][j],dp[i-1][st]+cst*(i-1));// i要-1 ,不要读错题
			}
		}
	}
	for(int i=1;i<=n;i++)ans=min(ans,dp[i][(1<<n)-1]);
	cout<<ans;
	return 0;
}

三进制状压做法

口胡一下,有点难写。

三进制数,该位为 0 代表没有开辟,1 代表早就开辟,可以转移;2 表示刚开辟,不能转移。

这种做法依然要记录层数。除此之外还要记录 3 的次幂之类的东西,常数极大。

注意转移时只要转移最高位的 1,因为其他位的 1 以后一定会循环到,避免了重复枚举。跟愤怒的小鸟那题挺像的。

复杂度 O(n2×3n),代码就不放了。

posted @   KS_Fszha  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示