状压dp

0.位运算

1.概述

用01数字标志状态

2.要求

  • 对象状态只能有两种,例如放/不放,正/反等等

  • 某一项指标的范围很小

3.实际运用

后续\(S_i\)一般表示状态(除特殊说明)

特殊方格棋盘

click

组合:我会!\(n!\)

先考虑所有格子都能放

\(n \leqslant 20\),可以状压

\(0\)表示没放,\(1\)表示放了

由于位运算的尿性,最右边是第一列,从右往左算

对于状态\(01101\),就表示放了\(1,3,4\)

由于一行只能放一个,上述状态表示第三行,那么

\[f(01101) = f(01100) + f(01001) + f(00101) \]

更进一步的,对于数\(s\),如果某一位是\(1\),那么就可以从这一位为0的状态转移而来

用位运算就是:

\[f_s +=f_{s - 2^i}(s >> i \& 1 = 1) \]

答案就是\(f(11111) = f(2^n-1)\)

但有的格子不能放了

那么首先,我们把不能放的状态也压缩

比如\(a[3]=00100\)就表示第三行第三列不能放

然后对于枚举的状态\(s\),进行如下操作:

\[s \& (\sim a_i) \]

解释:\(a_i\)取反后被标记为不能放的列一定是\(0\),再按位与就让\(s\)中同列的位置也变为\(0\),就相当于不能放

更多的,对于快速求得哪一位是\(1\),可以使用\(lowbit\) 啊?

for(int i = 1;i <= m;i++)
{
	scanf("%d%d",&x,&y);
	a[x] |= (1 << (y - 1));// 把不能放的位置标记为1 
}
f[0] = 1;
//cout << 114 << endl;
for(int i = 1;i <= res;i++)
{
	int u,num = 0;
	for(u = i;u;u -= lowbit(u)) num++;//统计1的个数,也就是当前的i表示第几行 
	u = i & (~a[num]);//把不能放的位置变成0 
	while(u)
	{
		//cout << 114 << endl;
		//用lowbit提取u中的1,即100... 
		f[i] += f[i ^ lowbit(u)];// 只有i的这一列不为1才能加 
		u -= lowbit(u); 
	}
}

P1879 [USACO06NOV] Corn Fields G

click

首先将能不能种草压缩

for(int i = 1;i <= n;i++)
{
	for(int j = 1;j <= m;j++)
	{
		int x;
		scanf("%d",&x);
		a[i] = (a[i] << 1) + x;
	}
}

\(dp_{i,s}\)表示第\(i\)行状态为\(S\)时的方案数

对于枚举的状态\(S\),如果存在\(11\)之类的非法情况,那么

\[S \& (S << 1) = 1 \]

为了避免上下相邻,我们取得\(i-1\)行的状态\(S'\),如果上下相邻,那么二者必有一位均是1,即

\[S \&S' = 1 \]

方程就是

\[dp_{i,S}+=dp_{i-1,S'}(S\&S'!=1) \]

for(int i = 1;i <= n;i++)
{
	for(int j = 0;j <= (1 << m) - 1;j++)
	{
		int u = j;
		if(u & (u << 1)) continue;//排除左右相邻
		if(a[i] == (a[i] | u))
		{
			vis[i][u] = 1; //标记合法,留着后面取用
			for(int k = 0;k <= (1 << m) - 1;k++)
			{
				if(k & (k << 1)) continue;
				int v = k;
				if(!vis[i - 1][v]) continue;//不合法
				if(u & v) continue;//排除上下相邻
				dp[i][u] = (dp[i][u] + dp[i - 1][v]) % mod;
			}
		}	
	}
}

P1896 [SCOI2005] 互不侵犯

click

\(0/1\)表示有没有放国王

\(dp_{i,j,S}\)表示放到第\(i\)行,用了\(j\)个国王,第\(i\)行的状态为\(S\)

接下来看转移条件

如果一个国王在第\(i\)行放在了第\(j\)列,那么对第\(i + 1\)行来说,第\(j - 1,j,j + 1\)列均不能放国王

接下来考虑用位运算排除情况

设当前第\(i\)行状态为\(S_i\),上一行状态为\(S_{i-1}\)

如果第\(j-1\)列有国王,可以右移一下

\[(S_i >> 1) \& S_{i - 1} = 1 \]

类似可得:

\(j\)列有国王:\(S_i \& S_{i-1} = 1\)

\(j + 1\)列有国王:\((S_i << 1)\&S_{i-1} = 1\)

转移条件就是上述三个式子的值均不为1

考虑方程式

发现前两维很像背包

\(i\)行放的棋子就是\(S_i\)\(1\)的个数,设为\(num_{S_i}\)

\[dp_{i,j,S_i} += dp_{i - 1,j - num_{S_i},S_{i-1}} \]

初始化:\(dp_{0,0,0} = 1\)

技巧:我们可以预处理出来一堆左右不相邻的合法状态备用,可以避免盲目暴力

P2704 [NOI2001] 炮兵阵地

click

又一道经典的入门题

放为\(1\),不放为\(0\)

根据炮兵射程可得

  • 连续三格内只能有一个阵地

横向判定就是分别左移一位和两位再分别取与

\[(S << 1) \& S=(S << 2) \&S = 0 \]

纵向判定涉及到三行,所以\(dp\)中要多一些行的状态

\(dp_{i,S_i,S_j}\)表示到了第\(i\)行,当前行的状态是\(S_i\),上一行的状态是\(S_j\)

那么转移就是\(dp_{i-1,S_j,S_k} \to dp_{i,S_i,S_j}\),判定的三行分别是\(S_i,S_j,S_k\)

由于连续三格在同一列,所以直接取与就行

\[S_i\&S_j=S_i\&S_k=S_j\&S_k = 0 \]

转移方程:

\[dp_{i,S_i,S_j} = \max{(dp_{i-1,S_j,S_k})} \]

取答案时要遍历所有可能状态

由于第一、二行上面不足两行,需要单独处理

for(int i = 1;i <= tot;i++)
{
	if((a[1] | S[i]) == a[1])
	{
		vis[1][S[i]] = 1;
		dp[1][S[i]][0] = num[S[i]];
	}
}//第一行,就是初始化
for(int i = 0;i <= tot;i++)
{
	if((a[2] | S[i]) == a[2])
	{
		int si = S[i];
		vis[2][S[i]] = 1;
		for(int j = 0;j <= tot;j++)	
		{
			int sj = S[j];	
			if(!vis[1][sj]) continue;
			if((si & sj) == 0)
				dp[2][si][sj] = max(dp[2][si][sj],dp[1][sj][0] + num[si]);	
		}
	}
}//第二行
for(int r = 3;r <= n;r++)
{
	for(int i = 0;i <= tot;i++)//第r行状态
	{
		int si = S[i];
		if((a[r] | si) == a[r])
		{
			vis[r][si] = 1;
			for(int j = 0;j <= tot;j++)//第r-1行状态
			{
				int sj = S[j];
				if(!vis[r - 1][sj]) continue;
				if(si & sj) continue;
				for(int k = 0;k <= tot;k++)//第r-2行状态
				{
					int sk = S[k];
					if(!vis[r - 2][sk]) continue;
					if(((sj & sk) | (si & sk)) == 0)
							dp[r][si][sj] = max(dp[r][si][sj],dp[r - 1][sj][sk] + num[si]);
				}
			}
		}
	}
}
int ans = -1;
for(int i = 1;i <= tot;i++)
	for(int j = 1;j <= tot;j++)
		ans =  max(ans,dp[n][S[i]][S[j]]);//遍历所有情况
  • \(Tip:\)为了避免内存危机,可以定义\(dp_{i,j,k}\)为当前为第\(i\)行,该行状态是合法状态中的第\(j\)个,上一行是合法状态中的第\(k\)

把代码中的\(si,sj,sk\)改成\(i,j,k\)就行

P3226 [HNOI2012] 集合选数

click

乍一看wc这是状压?

我们只能用\(01\)表示选没选,不能知道具体内容,所以对于二倍三倍的要求是很难办到的

那么我们能否想一个办法,使得当第\(i\)位为\(1\),第\(2i\)\(3i\)位就自动不可能为\(1\)了呢

也就是说,这种构造下的每一次选择伴随着舍弃两个选择

这种构造其实早就见过——

棋类问题

这种问题就是在某处放了棋后自动舍弃掉了会被攻击到的格子,符合需求

所以考虑构造一个矩阵来实现类似下棋的操作

不妨把二倍放到右边,三倍放到下边,就是

\[\begin{bmatrix} 1&2&4&8...\\ 3&6&12&24...\\ ... \end{bmatrix} \]

设定一个棋子的攻击范围就是它的右边和下边,那么问题就转化成了有多少合法放棋方案

但这样有一个问题:比如\(n = 20\)

\[\begin{bmatrix}1&2&4&8&16\\3&6&12\\9&18\end{bmatrix} \]

一些数进不去,一些位置空着

再次类比棋盘问题,我们还做过特殊方格棋盘,就把空位设置成不能放置棋子来解决

进不去的数呢?

再多搞几个棋盘,不在同一个棋盘上的数一定互不影响,所以合法

这样问题就成了

有若干张棋盘,每张棋盘上都有格子不能放棋,现在要在每个棋盘上放置若干枚棋子,满足所有棋子不能互相攻击到(当然,不在一个棋盘上的棋子肯定不能互相攻击)攻击范围就是右和下,求合法方案数

对于每张棋盘使用状压求解,使用乘法原理联结各个棋盘的答案得到方案数

绝了

坑点:memset会超时四个

预处理:

void init(int x)
{
	//不同于dp,这里面全是赋值类操作,所以不用memset
	r = 0;
	for(int i = 1;i <= 20;i++)
	{
		if(i == 1) a[i][1] = x;//特殊处理左上角
		else a[i][1] = a[i - 1][1] * 3;
		if(a[i][1] > n) break;
		vis[a[i][1]] = 1;//标记数字已被放入某个棋盘
		r = i;//记录最大行数,dp要用
		int num = 1;
		for(int j = 2;j <= 20;j++)
		{
			a[i][j] = a[i][j - 1] * 2;
			if(a[i][j] > n) break;
			num = j;
			vis[a[i][j]] = 1;
		}
		maxx[i] = (1 << num) - 1;//这里记录的是每行的上限,就是每行有多少个连续的1(可放棋子的位置)
	}
}


每个棋盘的dp:

```cpp
ll getans()
{
	//S[i]是预处理的合法状态
	for(int i = 1;S[i] <= maxx[1];i++) dp[1][i] = 1;//合法状态不能超过极限
	for(int i = 2;i <= r;i++) 
	{
		for(int j = 1;S[j] <= maxx[i] && j <= tot;j++)
		{
			dp[i][j] = 0;//由于不能使用memset,就用一个清零一个
	 		for(int k = 1;S[k] <= maxx[i - 1] && k <= tot;k++)
	 		{
	 			if(S[j] & S[k]) continue;
	 			dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;//朴素的dp
			}
		}
	}
	ll res = 0;
	for(int i = 1;S[i] <= maxx[r];i++)
		res = (res + dp[r][i]) % mod;
	return res;
} 

联结答案:

	for(int i = 1;i <= n;i++)
	{
		if(!vis[i]) //未使用,就构建新棋盘
		{
			init(i);
			ll sum = getans();
			ans = ans * sum % mod;//乘法原理
		}
	}

可见状压的一大基础模型就是棋盘类问题,这也是状压最本质的呈现

接下来看一些脱离棋盘的问题:

P2831 [NOIP2016 提高组] 愤怒的小鸟

click

写这题前趴电脑前面睡了半个小时,可能是被状压NaOH的昏迷了

\(n\leqslant 18\),考虑用\(0,1\)表示猪的死活,\(0\)表示没死,\(1\)表示死了

\(dp_{S}\)表示达到状态\(S\)所需的最少鸟数

那么

\[dp_S=\min{(dp_{S'}+num_{S'})} \]

显然,死的猪复活不了,所以\(S'\)\(1\)的个数在\(S\)的基础上要多,设多出来\(k\)\(1\),那么\(num_{S'}\)就表示打死那\(k\)只猪所需的最少鸟数

更具体的,由于抛物线的形式,所以两个点就能确定一个抛物线,那么设\(P_{i,j}\)表示经过第\(i,j\)个点能杀死的猪的集合,和\(S\)相同,杀死的猪在\(P_{i,j}\)中对应的数位为\(1\)

那么

\[dp_{S|P_{i,j}} = \min(dp_S+1) \]

但也可能存在需要额外消耗一只鸟(说明过这个点的抛物线上只有一个点\(k\))的情况,还得再处理一下

\[dp_{S|(1<<(k-1))} = \min(dp_S+ 1) \]

接下来考虑一些细节

  • \(P_{i,j}\)

\((x_1,y_1),(x_2,y_2)\)在同一条抛物线上,那么

\[\begin{cases} ax_1^2+bx_1 = y_1\\ ax_2^2+bx_2 = y_2 \end{cases} \]

解得

\[ a=\large\frac{y_1x_2-y_2x_1}{x_1x_2(x_1-x_2)}, b = \large\frac{y_1x_2^2-y_2x_1^2}{x_1x_2(x_2-x_1)}\]

int init(int p,int q)
{
	double x1 = point[p].x,x2 = point[q].x;
	double y1 = point[p].y,y2 = point[q].y;
	if(x1 == x2) return 0;//会导致a变成nan
	double a = (y1 * x2 - y2 * x1) / (x1 * x2 * x1 - x1 * x2 * x2);
	double b = (y1 * x2 * x2 - y2 * x1 * x1) / (x1 * x2 * x2 - x1 * x2 * x1);
	//cout << a << endl;
	if(a >= 0) return 0;//题目要求
	int s = 0;
	for(int i = 1;i <= n;i++)
	{
		double x = point[i].x,y = point[i].y;
		if(fabs(a * x * x + b * x - y) < eps)//实数误差在允许范围内
		{
			s |= 1 << (i - 1);
			dp[s] = dp[1 << (i - 1)] = 1;//当前的猪和目前找到的在一条线上的猪都能用一只鸟打掉
		}
	}
	return s;
}
  • 跳过不必要的枚举

我们需要一个\(i\)来枚举状态,需要\(j,k\)枚举\(P_{j,k}\),总复杂度是\(O(Tn^2\times(2^n-1))\),极限\(2e9,\)可能会超时(这么说是因为不加此处的优化好像也能过)

显然,使用\(P_{j,k}\)的前提就是第\(j,k\)只猪都活着,所以得保证当前\(i\)的第\(j,k\)位均不为\(1\)

for(int i = 0;i <= (1 << n) - 1;i++)
{
	for(int j = 1;j <= n;j++)
	{
		if((i >> (j - 1)) & 1 == 1) continue; //第j只猪是活的
		for(int k = 1;k <= j;k++)
		{
			if((i >> (k - 1)) & 1 == 1) continue;//第k只猪是活的
			dp[i | S[j][k]] = min(dp[i | S[j][k]],dp[i] + 1);
		}
		dp[i | (1 << (j - 1))] = min(dp[i | (1 << (j - 1))],dp[i] + 1);
	}
}

我才不说那个把\(double\)老写成\(int\)的傻子是谁 (我)

P4163 [SCOI2007] 排列

click

\(|s| \leqslant 10\),状压字符串

\(01\)表示某一位数字有没有被使用,\(0\)表示没用,\(1\)表示用了

接下来定义\(dp\)状态

发现“整除”这个东西挺不好保证的,尤其是在只知道位的情况下更不好办了

为了解决这个问题,不妨引进余数,整除就是余数为\(0\)的情况

故设\(dp_{S,d}\)表示状态为\(S\),余数为\(d\)时的方案数

如果要使用第\(i\)个数,那么根据\(dp\)尿性,前\(i-1\)位已经排好(但具体数字可能不是前\(i-1\)个数),应当放到第\(i\)位(从右往左),那么由余数可加性可得新余数就是\((10\times j + s_i) \% d\),其中\(j\)就是前 \(i-1\)位模\(d\)的余数

所以得到方程

\[dp_{S|(1<<(i-1)),(10\times j+s_i) \% d} = \sum_{0\leqslant j \leqslant d-1}dp_{S,j} \]

\[ans = dp_{(1 << |s|)-1,0} \]

由上一道题可知第\(i\)位必须是\(0\)

坑点:需要对序列去重,或者打标记防重复

if(vis[c[j] - '0']) continue;//该数字已被使用
if(i & (1 << (j - 1))) continue;//第j位被用了
vis[c[j] - '0'] = 1;//打标记

P2167 [SDOI2009] Bill的挑战

click

\(N \leqslant 15\),状压每个字符串是否与\(T\)匹配,\(1\)表示配上了,\(0\)表示没配上

很明显,状态中最多只能有\(K\)\(1\)

但有些\(S\)是一定不能同时匹配上一个\(T\)的,可以预处理

接下来就是定义\(dp\)状态

\(dp\)中至少有一维是\(S\),考虑另一位放什么

由于每一位放不同的字母会产生不同的效果,所以考虑用一维枚举放到了第几位

所以得到\(dp_{i,S}\)表示当前在第\(i\)位,匹配状态为\(S\)的方案数

答案就是

\[\sum_{num(S)=K}dp_{n,S} \]

\[num(S)表示S中1的个数 \]

方程就是

\[dp_{i,S} += dp_{i-1,S'} \]

\[其中S'在第i位放上某一字符时可以转化为S \]

接下来细嗦预处理并具体化\(dp\)

\(vis_{i,j}(1\leqslant j \leqslant 26)\)表示第\(i\)位放了第\(j\)个字符时仅第\(i\)的匹配情况(\(01\)表示)

for(int i = 1;i <= l;i++)
{
	for(int j = 1;j <= 26;j++)
	{
		for(int k = 1;k <= n;k++)
		{
			char op = s[k][i];
			if(op == '?') vis[i][j] |= (1 << (k - 1));
			else
			{
				if(op - 'a' + 1 == j) vis[i][j] |= (1 << (k - 1));
				else continue;
			}
		}
	}
}

那么结合\(dp\)可得

\[dp_{i,S\&vis[i][j]} += dp_{i-1,S} \]

按位与就表示只有前\(i-1\)位配上了(\(S\)对应位为\(1\))并且第\(i\)位也能配上(\(vis[i][j]\)对应位为\(1\))该串状态才可能为\(1\)

初始化:\(dp_{0,2^n-1} = 1\)

dp[0][(1 << n) - 1] = 1;
for(int i = 1;i <= l;i++)
	for(int s = 0;s <= (1 << n) - 1;s++)
			for(int j = 1;j <= 26;j++)
				dp[i][s & vis[i][j]] = (dp[i][s & vis[i][j]] + dp[i - 1][s]) % mod;	

听说还有容斥做法,。先咕咕

Disease Management

click

好像比前面都裸

把每头牛的得病状态压缩,类似上面的方程,把与改成或就行

坑点:二维要先继承上一维的状态再\(dp\)

for(int i = 1;i <= n;i++)
{
	for(int s = 0;s <= (1 << d) - 1;s++)
	{
		dp[i][s] = dp[i - 1][s];//先继承
		dp[i][s | a[i]] = max(dp[i][s | a[i]],dp[i - 1][s] + 1);
	}
} 

P3451 [POI2007] ATR-Tourist Attractions

图论+dp

题面有点扯淡

原题翻译核心:

最短的方案是\(1\to2\to4\to3\to4\to5\to8\),总长度为19,注意FGO为了从城市2到城市3可以经过城市4(\(2\to4\to3\)),但不在城市4停留

清新了

也就是说,\(2\to4\to3\)两条道路(边) 可以变成\(2\to3\)这一条路径

这样一来,我们就可以使用最短路算法处理出\(2\sim k+1\)到其他点的最短路并记录,相当于用\(2\sim k+1\)建了个新图,每一条边都是最短路径 (最小生成图)

\(d_{i,j}\)表示从\(i\)\(j\)的最短路径

建了新图之后,就只剩下了\(1 \sim k + 1,n\)等关键点,刚好\(K \leqslant 20\),直接状压这些点走没走

接下来考虑预处理\(g\)个限制

这一点可以参考集合选数,把每一个点的“上限”状态处理出来

\(dp_{i,S}\)表示当前在\(i\),状态为\(S\)的最短路

首先新图联通,所以点两两可达,直接暴力枚举

假设要从\(j\)走到,那么\(i\)就要满足

  • 对应数位为\(0\)

  • 要在\(i\)之前停留的数对应的数位均为\(1\)

那么

\[dp_{i,S} = \min\limits_{2 \leqslant j \leqslant k + 1}(dp_{j,S'}+d_{j,i}) \]

其中

\[S'\&S == S:保证限制的点已经走过 \]

\[S \&(1 << (i - 1)) == 0:保证点i没走过 \]

答案统计:

\[ans = \min\limits_{1 \leqslant j \leqslant k + 1}(dp_{j,S}+d_{j,n}),其中S的1-k+1位均为1 \]

一交一片黑,发现————

内存危机:64MB!!!

我突然释怀的亖了

是时候拿出我不会的必杀技了————————

滚动数组

我们发现第二维按照原开法最大是\(1 << (k + 1)-1\),但这之中有很多冗余状态,考虑优化

显然,如果\(S\to S_1 \to S_2 \to...\)成立,那么状态中\(1\)的数量一定单调增(旧的点走过了,不断在走新的点)

进一步的,相邻的转移状态中间\(1\)的个数一定只差了\(1\),因为一次只能走一个点

所以可以只存储相邻两层的信息,上一层是含有\(i\)\(1\)的方案,下一层就是含有\(i+1\)\(1\)的方案

就是开成\(dp_{i,S,2}\),用异或跳层数

中间\(S\)的大小取决于该层状态含\(1\)的个数,如果含有\(x\)\(1\),那么最大就是\(C_{20}^{x}\)

显然,最大是\(C_{20}^{10} = 184756\)

相比\(2^{20}-1=1048575\)是极大地优化

//由于题目特点,提前k++了
//这里将点的编号统一-2是方便与位运算兼容,直接压的是2~k+1
for(int i = 0,num,now;i < 1 << (k - 1);i++)
{
	for(num = 0,now = i;now;now -= lowbit(now),num++);
	newadd(num,i);
	id[i] = ++ sum[num];
}//用前向星的方式将含1个数与对应集合连接起来
for(int p = h[1];p;p = Re[p].ne)//遍历含1个1的集合
{
	int v = Re[p].to;
	for(int i = 2;i <= k;i++) 
	{
		if(v & (1 << (i - 2)))
		{
			dp[i][id[v]][0] = d[i][1];
			break;
		}
	}
}
//方向:j->i
for(int num = 2;num < k;num++,l ^= 1/*l控制层数*/)
{
	for(int o = h[num];o;o = Re[o].ne)
	{
		int v = Re[o].to;
		for(int i = 2,now;i <= k;i++)
		{
			if((v & (1 << (i - 2))) && ((S[i] & v) == S[i]))//判断是否满足条件
			{   
				now = v ^ (1 << (i - 2));//j循环中i点还没走过
				dp[i][id[v]][l] = 1 << 30;
				for(int j = 2;j <= k;j++)
				{
					if((now & (1 << (j - 2))) && ((S[j] & now) == S[j]))
                      				dp[i][id[v]][l] = min(dp[i][id[v]][l],dp[j][id[now]][l ^ 1] + d[j][i]);
				}
			}
		}
	}
}
for(int o = h[k - 1];o;o = Re[o].ne)
{
	int v = Re[o].to;
	for(int i = 2;i <= k;i++)
	{
		if((v & (1 << (i - 2))) && ((S[i] & v) == S[i]))
			ans = min(ans,dp[i][id[v]][l ^ 1] + d[i][n]);
	}
}//走完所有关键点后前往n点

P3622 [APIO2007] 动物园

click

先明确状压什么

发现题目中唯一的较小值是“连续的5个围栏”,考虑状压连续的5个围栏的状态

套路的设出\(dp_{i,S}\)。第一维表示当前所在围栏,第二维表示\(i\sim i + 4\)这5个围栏的状态,\(0\)表示移走,\(1\)表示还在,由\(dp_{i - 1,S'}\)转移答案

由于重叠关系,\(S\)确定后,\(i\sim i + 4\)位均已知,只有第 \(i-1\) 位是放/不放,在此处比个 \(max\)后加上 \(S\)能使有多少孩子开心

接下来考虑\(S'\)\(S\)的关系

\(S'\)的后四位就是\(S\)的前四位,即\(S >> 1\),所以如果第\(i - 1\)位放就是

\[S' = (S >> 1) | (1 << 4) \]

不放的话就不按位或了

所以

\[dp_{i,S} = \max(dp_{i - 1,S'}) + num \]

显然\(num\)可以预处理

仿照dp数组定义\(num_{i,S}\)表示\(i\sim i + 4\)的围栏状态为\(S\)时高兴的小朋友个数

我们尝试把小孩儿的喜恶也压成一个五位的二进制,但每个动物有三种状态:喜欢,讨厌,和无感

那就设置两个数\(x,y\),一个压喜欢,一个压不喜欢,共同点就是有感为1,无感为0,位数对应要和dp式兼容不然会出问题

但是考虑到转圈,沿用石子合并旧法,小于\(E\)的多加一个\(n\)即可

void init()
{
	int li,dis;
	li = 0;
	dis = 0;
	for(int i = 1;i <= f;i++)
	{
		int x;
		scanf("%d",&x);
		if(x < e) x += n;
		int u = 5 - (x - e);
		li |= 1 << (u - 1);//dp式的位是从左至右,但是位运算是从右往左,要转换
	}
	for(int i = 1;i <= l;i++)
	{
		int x;
		scanf("%d",&x);
		if(x < e) x += n;
		int u = 5 - (x - e);
		dis |= 1 << (u - 1);
	}
	for(int s = 0;s <= 31;s++)
	{
		bool f1 = (s & li);//要求对应位至少有一个1
		bool f2 = ((~s) & dis);//要求对应位至少有一位是0,所以先取反
		if(f1 || f2) num[e][s]++;
	}
}

初始化:限定\(0\sim 4\)的状态,对于每个状态做一次dp

坑点:限定\(0\sim 4\)就是限定了\(n,1,2,3,4\),所以取答案时不能遍历

int u = (1 << 4);
int ans = -1;
for(int S = 0;S <= 31;S++)//0-4位的状态
{
	memset(dp[0],-inf,sizeof(dp[0]));//其他状态0初始化为极小值
	dp[0][S] = 0;//当前状态初始
	for(int i = 1;i <= n;i++)
		for(int s = 0;s <= 31;s++)
			dp[i][s] = max(dp[i - 1][(s >> 1)],dp[i - 1][(s >> 1) | u]) + num[i][s];
	//for(int s = 0;s <= 31;s++)//不遍历
	ans = max(ans,dp[n][S]);//终末状态就是限定的初始态
}
posted @ 2024-02-28 20:51  why?123  阅读(11)  评论(0编辑  收藏  举报