状态压缩dp

相关技巧

  • 枚举子集:如果一个集合状态 S 由其所有子集 S0S 转移得到,这样转移的时间复杂度为 i=0n(ni)2i=3n
for(int S0 = S; S0; S0 = (S0 - 1) & S) {
{
	...
}
  • 高维前缀和

考虑二维前缀和还可以怎么计算:对于每一维,枚举剩下所有维的所有可能,计算关于该维的前缀和。

高维前缀和就是这样的原理。通常情况下每个维度大小为 2,维数为 n:枚举每一维 d,然后枚举所有状态(用二进制数来表示),如果当前状态 i 的第 d 位为 1,就加上 i2d 那个状态的值。

for(int d=0;d<n;d++)
    for(int i=0;i<1<<n;i++)
        if((i>>d)&1)f[i]+=f[i^(1<<d)];

时间复杂度 O(n2n)

HDU 4628 Pieces (子序列状压的一般套路)

题目

给定一个长度为 n 的字符串,一次操作定义为选出一个回文子序列并删掉,问将整个字符串删完的最少的操作次数

n16

题解

f[s] 表示将序列 s 完全删除的最小的操作次数,对于所有 s 为回文串的 f[s]=1 ,转移就是枚举子集转移 f[s]=minxsf[x]+f[sx],其中 x 是回文串,时间复杂度 O(3n)

HDU 6149 Valley Number II (图上状压一般思路)

题目

给定一个 n 个点 m 条边的无向图,其中 k 个点被标记为高点,nk 个点被标记为低点

一个山谷定义为一个三元组 <x,y,z> ,满足 x,z 是高点, y 是低点,而且 x,yy,z 之间均有边相连

如果一个点只能在一个山谷中,求这张图最多有多少个山谷

x30,kmin(n,15)

题解

首先,注意到 k 的范围是很小的,所以我们考虑将高点的使用情况进行状压

f[i][s] 表示考虑到第 i 个低点,使用了集合 s 中的高点,所能够形成的山谷有多少个

假设我们考虑到第 i 个点,要向 i+1 进行转移,那么就在图中找到所有与 i+1 相连的高点,然后转移即可

P3959 [NOIP2017 提高组] 宝藏 (分层法确定DP顺序)

题目

给定一个 n 个点 m 条边的无向图,边有边权。找到这张图的一棵生成树,记一个点到根距离为 L ,点的个数为 K ,这个点的贡献为 L×K ,要求最小化贡献和

题解

考虑根据离根的深度(从当前到根经过的点数),一层一层进行DP

f[i][s] 表示考虑到第 i 层,选了 s 集合中的点的最小贡献

转移为

f[i][s]=min{f[i1][s0]+cost(s0,s)×(i+1)}

其中 cost(s0,s) 表示的是从状态 s0s 的最短距离,可以预处理出来

时间复杂度为 O(3nn2)

P6622 [省选联考 2020 A/B 卷] 信号传递 (状态的设计及优化)

题目

题解

按题意,我们暴力枚举这 m 个信号站的排列顺序,时间复杂度 O((m!)n)

容易发现,题目给出的这个长度为 n 的序列S具体是什么不重要,重要的是,每一对信号站 i,j ,在 S 里作为相邻的位置,出现了多少次。也就是有多少个位置 p(1p<n) 满足 Sp=i,Sp+1=j 。我们记这个数量为 cnt[i][j]

对于任意 ijcnt[i][j] 就表示要从信号站 i ,向信号站 j,进行多少次传递。设两个信号站的位置,分别为 pos[i],pos[j],则它们会对答案产生的代价就是:

{pos[j]pos[i](pos[i]<pos[j])k(pos[i]+pos[j])(pos[i]>pos[j])

这样我们就干掉了 n 的限制,时间复杂度为 O(n+(m!)m2)

接下来我们有两种状压DP的状态设计

  • 按位置考虑:设 f[s] 表示考虑完前 |s| 个数,填了集合 s 中位置
  • 按值考虑:设 f[s] 表示考虑完前 |s| 个位置,填了集合 s 中的数

这是状压DP中两个极为常见的状态设计,而本题适用于第二种状态设计

为了方便转移,我们将点对的贡献拆解为单点的贡献,具体来说,假设我们考虑到第 pos 个位置:

  • 对于一个前面的数 j ,从 ij 产生的代价是:poskcnt[i][j]
  • 对于一个前面的数 j ,从 ji 产生的代价是:poscnt[j][i]
  • 对于一个后面的数 j,从 ij 产生的代价是:poscnt[i][j]
  • 对于一个后面的数 j,从 ji 产生的代价是:poskcnt[j][i]

转移方程就是

f[s+i]f[s]+posjs(kcnt[i][j]+cnt[j][i])+posj(s+i)(cnt[i][j]+kcnt[j][i])

如果枚举 i ,再枚举 j,DP的时间复杂度 O(2mm2)

我们继续优化。考虑只枚举 i。把 pos前的系数(也就是所有 j 的贡献之和),预处理出来,不妨记为:cost(s,i)。那么上述的转移式,也可以改写为:

f[s+i]f[s]+poscost(s,i)

考虑预处理 cost(s,i)。首先,根据定义,is

我们考虑,cost(s,i),可以从“ s 去掉某个数”的状态,转移过来。我们不妨就去掉 j=lowbit(s)。那么,

cost(s,i)=cost(sj,i)+(kcnt[i][j]+cnt[j][i])(cnt[i][j]+kcnt[j][i])

这样转移是 O(1) 的。所以预处理的时间复杂度为 O(2mm),DP的时间复杂度也降为 O(2mm)。总时间复杂度 O(n+2mm)。但是 cost 数组会占用 O(2mm) 的空间,这无法承受。所以还要继续优化空间。

发现 costf 的转移,都是按集合从小到大进行的。所以我们不妨就按这个顺序,一边DP,一边求 cost

对每个 s,我们把 cost(s,) 视为一个大小为 m 的数组。当前的 s,我们先做DP转移,再拿 cost(s) 数组去更新所有 cost(s)。发现更新完之后,cost(s,) 这个大小为 m 的数组,就可以删掉

可以采取滚动数组的方式来实现,这样空间复杂度也符合要求了,可以通过本题

P1777 帮助 (普通状压dp)

题目

定义一个长度为 n 的序列的混乱度为序列中不相等连续段的个数,序列的极差不超过 7 现在你可以从序列中删掉 k 个值,求最小的混乱度

n,k100

题解

首先离散化一下,得到一个新的数组 h,h 的值域为 07

f[i][j][k][l] 表示考虑到第 i 个位置,删去了 j 个数,选的数的集合为 k ,最后一个没有被删的数为 l ,那么显然有转移

  • 如果第 i 个位置没有被删

f[i][j][k|(1<<hi)][hi]=min{f[i1][j][k][l]+[l=hi]}

  • 如果第 i 个位置被删

f[i][j+1][k][l]=min{f[i1][j][k][l]}

时间复杂度 O(mk×8×28)

[BZOJ 1231] mixup2 (普通状压dp)

题目
Farmer JohnN头奶牛中的每一头都有一个唯一的编号Si. 奶牛为她们的编号感到骄傲, 所以每一头奶牛都把她的编号刻在一个金牌上, 并且把金牌挂在她们宽大的脖子上. 奶牛们对在挤奶的时候被排成一支"混乱"的队伍非常反感.
如果一个队伍里所有两头相邻的奶牛的编号相差超过K, 它就被称为是混乱的. 比如说,当N=6,K=11,3,5,2,6,4就是一支"混乱"的队伍, 但是1,3,6,5,2,4不是(因为56只相差1). 那么, 有多少种能够使奶牛排成"混乱"的队伍的方案呢?

题解

f[i][s] 表示考虑到第 i 头奶牛,奶牛的选取情况为 s 的方案数

转移时,如果当前枚举的 s 中不含 i 那么直接跳过,否则 f[i][s] 就可以从 f[j][s|(1<<(i1))] 转移

P5933 [清华集训2012]串珠子 (连通图中的状压dp)

题目

给定 n 个点,两个点之间有 c[i][j] 条本质不同的边,求构成连通图的方案数

题解

直接维护连通是是困难的,那么正难则反

f[s] 表示点集为 s 且连通的方案数, g[s] 表示点集为 s 时任意连边的方案数, h[s] 表示点集为 s 时不连通的方案数

显然g[s]=i,js(c[i][j]+1)

关于 h[s] ,可以任取 s 中的一个点 p,设与 p 连通的子集为 s1 ,与 p 不连通的子集为 s2,那么 h[s]=f[s1]×g[s2]

f[s]=g[s]h[s]

于是我们枚举子集转移即可

P2704 [NOI2001] 炮兵阵地 (多行影响的状压dp)

题目

N×M ( N100 , M10 ) 的网格地图上部署炮兵部队。每一格可能是山地或平原,一个炮兵部队的攻击范围是长宽为 5 的十字形,炮兵部队只能部署在平原。

求在炮兵部队之间不能互相攻击的前提下,最多能部署多少炮兵
部队。

题解

考虑到

  • 列数很小,可以状压。
  • 每个炮兵可以向上影响两行,状压一行是不够的。
  • 每个炮兵会向左右影响两个,每列放炮兵的方案不多。

于是我们可以状压两行,设 f[i][A][B],表示考虑到第 i 行,第 i1 行的状态为 A,第 i2 行的状态为 B 的方案数

直接转移就行

CF453B Little Pony and Harmony Chest (根据题目性质减少压缩状态的大小)

题目

给定长为 n 的数组 a[],要构造长为 n 的正数数组 b[],要求 b[] 中的数两两互质,且最小化

val=i=1n|aibi|

输出 b[]

1n100,1ai30

题解

若存在某 bi59,那就不如把 bi 改成 1 ,因为 biaiai1,且改成1不影响互质。

所以 1bi<59。所有 bi 的质因子分解式中,出现的质数只可能为 2,3,5,7,,53,共16个。

用状态 s 记录选了哪些质数。f(i,s) 表示考虑 a[1i],已经用过的质数集为 s 的最小 val 值。

枚举上一个的状态 s,再从 158 枚举现在要用哪个数,更新状态。记录 s 用于回溯

互质 (状压dp与背包的结合)

题目

n 个数,问这 n 个数里最多选出几个数,使得选出的数两两之间互质。

1n1000
11000

题解
33以内的质因数只有12
一个1000以内的数,超过33的质因数只可能有一个

记一个状态s表示那12个质数的选择情况

  • 对于任意一个数,只有33以内的质因数:这样的话,直接暴力转移即可

  • 33以上的质因数:将拥有相同的、大于33的质因数的数存成一组,分组背包转移

f[i][s]表示前i个数当中,选出了一些互质的数他们含有s里这些质因数的情况下,最多能选出的数的个数

复杂度O(n212)

BZOJ 5180 Cities (最小斯坦纳树)

题目

给定n个点,m条双向边的图。其中有k个点是重要的。每条边都有一定的长度。
现在要你选定一些边来构成一个图,要使得k个重要的点相互连通,求边的长度和的最小值。

k5

n105

1m2×105

题解

首先,答案子图一定是一棵树,因为如果有环,那么一定可以断掉一条边使答案更优且保持图的联通

那么我们设f[i][s]表示以i为根,选了s中的点的最优答案

  • 当点i度数为1,设与它联通的点是j,那么有转移

f[i][s]wi,j+f[j][s]

  • 当点i的度数大于1时,我们考虑枚举TS,那么有转移

f[i][s]f[i][ST]+f[i][T]

上面那个转移很像最短路算法的三角不等式,使用dijkstraspfa进行松弛操作转移O(mlogm2k)

下面那个转移可以采取枚举子集的方法进行转移O(n3k)

i联通的点每次转移都只会多一个,类似于背包,我们可以S从小到大枚举

每次松弛操作从j扩展到i时,如果i时关键点,没有算上i怎么办呢?不用管,因为后面一定有一次枚举子集转移时会把i算上

所以这个转移是正确,使用dij时间复杂度O(mlogm2k+n3k),不过这题的复杂度瓶颈并不在使用SPFA,因此使用SPFA会更快

[ATC 2230] Water Distribution (状压与最小生成树)

题目

在一个二维平面上有N个城市, 第i个城市的坐标是(xi,yi) , 一开始拥有的水量是ai
现在你可以从一个城市向另一个城市运送任意数量的水, 但水在运输过程中会有损耗, 具体而言如果从x城市运L水到y城市,最终y城市得到的水量是max(0,Ldis(x,y)), 其中dis(x,y)xy城市间的欧几里得距离。 你可以多次进行这个操作。
你要使最终水量最少的城市水量尽量多, 求这个值,精度误差不超过 109.

1N15

题解

枚举所有215次方种子集,对每一种子集求最小生成树检查一下是否能够送水,并求出最小的水量,记此时状态为f[s],s是此时点的选取情况,表示这个联通块的最小值为多少,时间复杂度O(2nn2logn)

然后对整个点集枚举子集的子集进行合并转移,时间复杂度是O(3n)

总时间复杂度是O(2nn2logn+3n)

[AGC 012E] Camel and Oases (根据题目性质巧妙选取压缩状态)

题目

数轴上有n个点,初始V=V0,每次可以从一个点走到与他距离不超过V的点。当V>0时也可以让V=V/2并瞬移到任意一个点。
对于i=1n问从i号点出发能否遍历所有点。

1n,V02×105

题解

首先V的取值只能有logV0种,对于每一种V的取值,它能够走的是一段段的连续的线段。所以我们可以考虑将图分成logV0层连续的线段。

考虑一下用什么样的方式统计答案,我们可以考虑枚举第一层的每条线段(li,ri),如果说存在一种方案使得从1开始覆盖到一个li1的位置,从n开始覆盖到一个ri+1的位置,那么这段线段中的每一个点都是Possible的,否则就是Impossible

此时那个分层线段有一个性质:对于不同层之间的线段,它们相互之间要么上面的完全包含下面的,要么完全不相交

所以这时我们可以考虑维护lp[s],rp[s]分别表示从1开始向右可以扩展到的最右端和从n开始向左的最左端

通过状压每一层是否被选过,我们向中间选取连续的线段。假设我们现在要放第i层的线段,那么我们就要找到上一个状态扩展最远的位置,然后进行转移

st(i)表示当前我们选的那一层,定义两个过程:mr(i,x)表示在第i层的线段中,严格大于x的第一个右端点,ml(i,x)表示在第i层的线段中,严格小于x的最靠近n的右端点

那么转移如下

lp[st|St(i)]=max{lp[st|St(i)],ml(i,lp[st])}rp[st|St(i)]=min{rp[st|St(i)],mr(i,rp[st]1)}

处理完这个,如上文所言,我们就直接暴力枚举第一层的线段,然后check就可以了

[SRM 713] DFSCount (树上状压)

题目

给定一个n个点的简单无向图,问DFS序总共可能有多少种不同情况?

N14
题解

f[i][s] ,表示以 i 为起点,走了 s 中点所产生的 dfs 序的个数

考虑 dfs 的过程,这显然是一棵树,并且只有把一棵子树中所有的点全部走完之后才会回溯到原来的点

那么对于状态 f[i][s] 的转移,我们可以使用并查集维护所有的连通块,当前点i可以转移到去掉i之后的所有连通块中,由于转移顺序比较神奇,所以我们直接记搜即可

CF1342F Make It Ascending (交换状态与值域)

题目

给定一个长度为 n 的序列 a,每次可以两个位置 i,j(ij),令 aj 等于 ai+aj 并将 ai 从序列中删除。

求将原序列变成严格单调上升的最少操作次数。

n15

题解

先将每个集合及其元素和预处理出来

f[i][p][s] 表示考虑了前 i 个集合,第 i 个集合剩下的那个元素位置是 p,已经使用的数组成的集合是 s 时的最小操作次数

转移时枚举集合 s 的补集,并入第 i 个集合或者新建第 i+1 个集合。保证新加入的第 i+1 个集合中元素和大于第 i 个集合且有在 p 之后的元素。时间复杂度为 O(n23n)

这道题涉及到方案的输出,因此给一个代码

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return x*f;
}
const int N=16,inf=0x3f3f3f3f;
int T,n,a[N],f[N][N][1<<N],id[N];
struct node{
	int i,p,s;
}las[N][N][1<<N];
int sum[1<<N];
inline void update(node u,node v,int k)
{
	f[v.i][v.p][v.s]=min(f[v.i][v.p][v.s],k);
	if(f[v.i][v.p][v.s]==k) las[v.i][v.p][v.s]=u;
}
inline void solve()
{
	n=read();
	for(int i=0;i<n;++i) a[i]=read();
	for(int s=0;s<(1<<n);++s)//预处理处每个集合中数的和 
	{
		sum[s]=0;
		for(int i=0;i<n;++i)
			if(s&(1<<i)) sum[s]+=a[i]; 
	}
	for(int i=0;i<=n;++i)
		for(int p=0;p<=n;++p)
			for(int s=0;s<(1<<n);++s)
				f[i][p][s]=inf;
	f[0][0][0]=0;
	for(int i=0;i<n;++i)
		for(int p=0;p<n;++p)
			for(int s=0;s<(1<<n);++s)
			{
				if(f[i][p][s]==inf) continue;
				node u=(node){i,p,s};
				int ns=((1<<n)-1)^s;//求s的补集 
				for(int s0=ns;s0;s0=(s0-1)&ns)//枚举s的补集 
				{
					if(sum[s0]>f[i][p][s]&&(s0>>p)!=0)//s0的数的和大于上一个集合,并且有在p之后的数 
					{
						node v=(node){i+1,p+1+__builtin_ctz(s0>>p),s|s0};
						update(u,v,sum[s0]);//记录dp路径,方便输出答案 
					}
				}
			}
	node ans=(node){-1,-1,-1};
	for(int i=n;i>=1;--i)
	{
		for(int p=1;p<=n;++p)
			if(f[i][p][(1<<n)-1]!=inf)
			{
				ans=(node){i,p,(1<<n)-1};
				break;
			}
		if(ans.i!=-1) break;
	}
	printf("%d\n",n-ans.i);
	for(int i=0;i<n;++i) id[i]=i+1;
	while(ans.i!=0)//输出方案 
	{
		node tmp=las[ans.i][ans.p][ans.s];
		int s0=tmp.s^ans.s;
		for(int i=0;i<n;++i)
		{
			if((s0&(1<<i))&&i!=ans.p-1)
			{
				printf("%d %d\n",id[i],id[ans.p-1]);
				for(int j=i+1;j<n;++j) id[j]--;
			}
		}
		ans=tmp;
	}
} 
int main()
{
	T=read();
	while(T--) solve();
	return 0;
}

P2150 [NOI2015] 寿司晚宴 (根据质因子缩减范围)

题目

题解

我们进行根号分治

n500 时,考虑小于 n 的就那 8 个,而大于 n 的质因子最多就只能有一个,所
以我们可以先用状压求出小于 n 的那一部分质因子,然后枚举大于 n 的质因子就可以了

具体来说,设 f[s1][s2] 表示两个人选的质因子集合分别为 s1,s2 时且让第一个人选大质因子的方案数,相应的 ,g[s1][s2] 表示让第二个人选大质因子的方案数,dp[i][j] 表示的是总方案数,显然有如下转移

{f[s1|k][s2]+=f[s1][s2]   (s2&k=0)g[s1][s2|k]+=g[s1][s2]   (s1&k=0)dp[s1][s2]=f[s1][s2]+g[s1][s2]dp[s1][s2]

后面需要减去一个 dp[s1][s2] 是因为两者都不选的情况被统计了两次

时间复杂度 O(n216)

NOIP 四校联测 Day3 数字重组 (对极差的处理方法+增加维数)

题目

给一个长度为 n 的序列 a,我们现在要将这 n 个数等分成 k 个集合,每个集合有 nk 个元素 (n mod k=0) ,每个集合中不能有重复的数,要求最大化集合中元素的极差之和

n70

题解

数据范围很小,又是个跟集合划分有关的问题,明显是个状压dp

先来考虑怎么处理极差。

众所周知极差 =maxmin ,所以我们从小到大考虑每个 a[i] ,将其放入每一个集合中。假设当前考虑到一个集合,我们要把一个数 x 放进去,当这个集合中为空时,就是对 min 有了 x 的贡献,总极差 x ;当这个集合只差一个就满时,就是对 maxx 的贡献,总极差 +x

于是我们设 f[i][s] 表示考虑到第 i 个元素,集合的选取状况是 s ,注这个 s 可以使用 vector 存,由于我们存元素的集合本质相同,所以可以将 s 中的数量从小到大排序,方便转移

注意到题目中要求我们保证各个集合中没有重复元素,所以我们要增一维 j ,表示上一次放在了哪个位置(代码中表示的是上一次加入 a[i1] 的那个集合中数的个数)。

然后特判+转移即可,详见代码

本题的复杂度证明是难点,注意到每个状态都能看作从 (0,0) 出发往右往上走到达 (k,nk)
的折线,由此可知状态数为 (k+nkk)(2nn)

所以总的时间复杂度是 O(nk(2nn))

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read()
{
	int x=0,f=1;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
	for(;isdigit(ch);ch=getchar()) x=(x<<3)+(x<<1)+(ch^48);
	return x*f;
}
const int N=110,M=60010,inf=1e18;
vector<int> S[M];
map<vector<int>,int> mp;
int n,k,tmp[N],cnt,sum[M],a[N];
int f[2][M][N];
inline void init(int x)
{
	if(x>k)
	{
		++cnt;
		for(int i=1;i<=k;++i)
			S[cnt].push_back(tmp[i]),sum[cnt]+=tmp[i];
		mp[S[cnt]]=cnt;
		return;
	}
	for(int i=tmp[x-1];i<=n/k;++i)
	{
		tmp[x]=i;
		init(x+1);
	}
}
signed main()
{
	freopen("num.in","r",stdin);
	freopen("num.out","w",stdout);
	n=read();k=read();
	for(int i=1;i<=n;++i) a[i]=read();
	sort(a+1,a+1+n);
	init(1);//预处理选择的状态 
	memset(f,inf,sizeof(f));
	f[0][1][0]=0;
	for(int i=1;i<=n;++i)
	{
		for(int s=1;s<=cnt;++s)
		{
			if(sum[s]!=i) continue;
			for(int j=0;j<=n/k;++j)
				f[i&1][s][j]=inf;
		}
		for(int s=1;s<=cnt;++s)//s-->x 
		{
			if(sum[s]!=i-1) continue;
			for(int j=0;j<=n/k;++j)
			{
				for(int p=0;p<k;++p)
				{
					if(p<k-1&&S[s][p]==S[s][p+1]) continue;//先填后面 
					if(S[s][p]==n/k) continue;//当前位置已满 
					vector<int> nw=S[s];
					++nw[p];//当前位置新增一员 
					if(a[i]==a[i-1])
					{
						if(S[s][p]>j) continue;
						int x=mp[nw];//对应的状态编号
						int val=0;
						if(S[s][p]==n/k-1) val+=a[i];//最大的 
						if(S[s][p]==0) val-=a[i];//最小的 
						f[i&1][x][S[s][p]]=min(f[i&1][x][S[s][p]],f[(i&1)^1][s][j]+val);
					}
					else
					{
						int x=mp[nw];
						int val=0;
						if(S[s][p]==n/k-1) val+=a[i];
						if(S[s][p]==0) val-=a[i];
						f[i&1][x][S[s][p]]=min(f[i&1][x][S[s][p]],f[(i&1)^1][s][j]+val);
					}
				}
			}
		}
	}
	printf("%lld\n",f[n&1][cnt][n/k-1]);
	return 0;
}

SDOI 一轮省集 哈密顿路 (交换答案与状态+无向图哈密顿路性质+lowbit优化转移)

题目

给定由 n 个点组成的无向图,对于图中得每一对点 x,y 判断是否存在以它们为起点终点的哈密顿路

n24

题解

f[x][y][S] 表示是否存在以 x 为起点,当前走到点 y ,走过的点构成的集合是 S 时的路径,这个东西的空间是 O(n22n) ,时间是 O(n32n) 的,显然无法通过本题

我们来思考一下这个 dp ,首先 x 没有参与 dp 的转移,因此这一维空间可以去掉, 对于每个 x 分别初始化然后转移即可。然后这种可行性 dp 显然可以考虑交换答案与状态的,于是我们更改 dp 状态为 f[S]=T 表示当前经过点集 S ,现在所处的位置可以是 T 。这个做法的空间复杂度为 O(2n) ,时间复杂度为 O(n22n) ,仍然无法通过本题

注意到无向图哈密顿路的一个性质——对于图中一点 i ,如果 xy 之间存在哈密顿路,即一定经过点 i ,路径就可以写成 xiy ,由于是无向图,上面那个就等价于找 ix,iy 这两条路径,两条路径不交且并集是全集,于是我们就可以钦定起点为任意一点(我选择了 n 这个点),转移时,我们可以考虑预处理出 nxt[S]=T 表示从点集 S 中出发可以到达的点组成的集合 T ,这里复杂度是 O(n2n) 的。转移即从 nxt[S]S 中的补集中转移到 S ,求出我们上文所说的 f[S] ,最后利用 f[S] 更新出答案,时间复杂度降为 O(n2n) ,可以通过本题。

值得注意的是,我们在枚举经过 S 集合中的点最终停留的点的位置,即 f[S] 时,可以使用 lowbit 来枚举停留的位置

具体代码如下

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return x*f;
}
const int N=24;
int n,m,ans[N],g[N][N];
char s[N];
int f[1<<N],nxt[1<<N];
inline int getS(int x)
{
	return (1<<x);
}
inline int lowbit(int x)
{
	return x&(-x);
}
int main()
{
	//freopen("hamilton.in","r",stdin);
	//freopen("hamilton.out","w",stdout);
	n=read();m=getS(n-1);
	for(int i=0;i<n;++i)
	{
		scanf("%s",s);
		for(int j=0;j<n;++j)
			if(s[j]=='1') nxt[getS(i)]^=getS(j);
	}
	for(int i=0;i<n;++i)
		for(int mk=0;mk<(1<<n);++mk)
		{
			if((mk>>i)&1) nxt[mk]|=nxt[mk^getS(i)];
		}
	for(int i=nxt[getS(n-1)];i;i-=lowbit(i)) f[lowbit(i)]=lowbit(i);
	for(int i=1;i<m;++i)
		for(int j=0;j<n-1;++j)
		{
			if((i>>j)&1) continue;
			if((nxt[f[i]]>>j)&1)
				f[i|getS(j)]|=getS(j);
		}
	ans[n-1]=f[m-1];
	for(int i=1;i<m;++i)
		for(int j=f[i];j;j-=lowbit(j))
		{
			int cnt=__builtin_ctz(j);
			ans[cnt]|=f[(m-1)^i];
		}
			
	for(int i=0;i<n-1;++i)
		for(int j=i+1;j<n;++j)
			if((ans[j]>>i)&1) g[i][j]=g[j][i]=1;
	for(int i=0;i<n;++i)
	{
		for(int j=0;j<n;++j)
			printf("%d",g[i][j]);
		puts("");
	}
		
	return 0;
}

Atcoder Beginner Contest 319 F (贪心+图上状压)

题意

你在一棵有 n 个点的树上战斗,起初你的战斗力是 1 ,树上有 k 个点是药剂,其余都是怪物。

对于第 i 个点有两个值 si,gi

  1. 如果这个点是怪物,那么如果你当前的战斗力不小于 si,你就可以增加 gi 的战斗力(注意这里比那个经典贪心简单,不用扣血)
  2. 如果这个点是药剂,那么你当前的战斗力就会乘上 gi

请判断你是否可以杀掉所有怪物

n500
si,gi109
k10

题解

假设没有药剂,那么考虑贪心,我们就开一个优先队列,每次走 si 小的那个怪物就行了

而如果有药剂,我们发现,如果能加那就一定要先加,先加再乘一定比先乘后加更优

那么我们就每次按照第一个贪心先加,走不动了就找那个 gi 最小的药剂,知道能走动为止,这样就可以了

吗?

考虑我们目前有两个可以使用的药剂,他们的回复量分别是 a,b (a<b) ,如果说,我们当前只用一个 b 可以走动,只用 a 不能走动,但按照我们上面的贪心,我们会先用了 a ,再用了 b ,由先加后乘一定比先乘后加更优,我们这里显然浪费了 a 的贡献,那么这个贪心就是错的

但是天无绝人之路,注意到原题中 k10 ,也就是药剂的数量很少,于是我们就可以考虑枚举药剂的顺序,按照顺序喝药,再跑上面的贪心,这样就是对的,但是时间复杂度 O(k!nklogn) ,我们无法接受

所以考虑状压,设 f[S] 表示选了集合 S 中的药剂后的战斗力最大值可以是多少,每次将当前集合可以连接到的药剂给记下来,并分别考虑加入每一个药剂,转移即可

时间复杂度 O(2knklogn)

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
const int N=510;
ll f[1<<10];
int n,m;
struct Edge{
	int v,nxt;
}edge[N];
int cnt,head[N];
inline void add_edge(int u,int v)
{
	edge[++cnt].v=v;
	edge[cnt].nxt=head[u];
	head[u]=cnt;
}
int s[N],g[N],iq[N];
const ll inf=1e9+10;
struct node{
	int u,w;
	bool operator < (const node &x) const{return x.w<w;}
};
int main(){
	memset(f,-1,sizeof(f));
	scanf("%d",&n);
	for(int i=2,fa,ty;i<=n;++i)
	{
		scanf("%d%d%d%d",&fa,&ty,&s[i],&g[i]);
		add_edge(fa,i);
		if(ty==2) iq[i]=++m;
	}
	priority_queue<node>q;
	q.push((node){1,s[1]});
	ll val=1;
	while(!q.empty()&&val>=q.top().w){
		int u=q.top().u;q.pop();
		val+=g[u];
		for(int i=head[u];i;i=edge[i].nxt)
		{
			int v=edge[i].v;
			if(!iq[v])
				q.push((node){v,s[v]});
		}
	}
	f[0]=val;
	for(int S=0;S<(1<<m);++S)
		if(f[S]>=0)
		{
			f[S]=min(f[S],inf);
			while(!q.empty())q.pop();
			q.push((node){1,s[1]});
			vector<int> tmp;
			while(!q.empty()&&f[S]>=q.top().w)
			{
				int u=q.top().u;q.pop();
				for(int i=head[u];i;i=edge[i].nxt)
				{
					int v=edge[i].v;
					if(!iq[v]||((S>>(iq[v]-1))&1))
						q.push((node){v,s[v]});
					else tmp.emplace_back(v);
				}	
				
			}
			if(S==(1<<m)-1)
			{
				if(q.empty()) puts("Yes");
				else puts("No");
				return 0;
			}
			for(int x:tmp)
			{
				ll np=min(inf,f[S]*g[x]);
				int T=S|(1<<(iq[x]-1));
				priority_queue<node> q2=q;
				for(int i=head[x];i;i=edge[i].nxt)
				{
					int v=edge[i].v;
					q2.push((node){v,s[v]});
				}
				while(!q2.empty()&&np>=q2.top().w)
				{
					int u=q2.top().u;q2.pop();
					np+=g[u];
					for(int i=head[u];i;i=edge[i].nxt)
					{
						int v=edge[i].v;
						if(!iq[v]) q2.push((node){v,s[v]}); 
					}
						
				}
				f[T]=max(f[T],np); 
			}
		}
	return puts("No"),0;
}

CF1886 E. I Wanna be the Team Leader (贪心+可行性转最优化)

题意

n 名员工和 m 个任务,第 i 个员工有工作能力 ai ,第 i 个任务有难度 bi 。每个员工至多参与一个任务

如果任务 ik 名员工完成,则要求 minakbi ,判断是否有办法让每个任务都被完成

n2×105,m20

题解

首先 m 的数据范围一看就可以状压,记 S 表示目前完成的任务。直接进行可行性 dp 是比较浪费的,于是我们设 fS 表示完成这些任务所需要的最小人数

考虑一个任务是否能够完成只与能力值最小的员工有关,因此我们可以先将员工按照能力值从小到大排序,那么每个任务都对应排序后 a 序列的一个连续区间一定不劣。

考虑转移,我们从小到大枚举每个员工是否是最小的,接着枚举我们现在要加入一个任务 bi ,设 j=fS ,则我们就要在 j+1n 尝试找下一个最小员工,如果找到的是第 j+x 个员工,那么根据定义,j+1j+x1 的员工不参与任务,第 j+x 个员工参与任务需要 biaj+x 的代价,于是转移方程即为

fS|2ifS+minx(biaj+x+x1)

后面的最值用 ST表 处理即可,时间复杂度 O(nm+m2m)

posted on   star_road_xyz  阅读(26)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8
点击右上角即可分享
微信分享提示