状态压缩DP-经典&变形

前言

状压 DP 就是一类使用根据几进制来进行状态转移方程。

一般来说,数据范围 2 n 2^n 2n 都在情理之中,可是 n ! n! n! 就直接爆炸。

状压一般来说都可以用搜索,并且搜索都可以拿分,只是拿不全。

所以说,一般看见有一维是 11 11 11~ 19 19 19 的话,那可以考虑状压。

题目选讲

Solution

经典的状压 DP,算是入门题目吧。

我们发现 n n n 和 m m m 都特别小,但是搜索肯定过不了,那么我们思考一下状压。

我们发现:每一行的状态可以用一个二进制来表示,每一块草地种或者不种。

而且当前这块草地仅仅受到前一行草地的影响,也就是无后效性,肯定就是打 DP。

我们设 d p i , j dp_{i,j} dpi,j​ 表示第 i i i 行状态为 j j j 的情况下,有多少种方案数。

这里, j j j 表示出来是十进制数,实际上它表示了一种二进制下的一种状态。

首先排除这一行本身不合法的情况,即存在相邻或者不能种的草地上种了草。

对于这种判断,像我这种蒟蒻只会先用个数组存下来,然后在暴力判断,带一点常数。

大佬们可以尝试用位运算试试,只是很容易出锅。

接着我们枚举上一行,然后再看两行是否有相邻,如果合法了,那么累加就行了。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=13;
const int mod=1e8;
int n,m,ans;
int a[N][N],Log[N],Tag;
int dp[N][1<<N];
bool check(int i,int j){
	if(j&(j<<1)) return 1;
	int w=m;
	while(j){
		if(j&1 && !a[i][w]) return 1;
		j>>=1;
		w--;
	}
	return 0;
}
int main(){
	scanf("%d %d",&n,&m);
	Tag=1<<m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
	for(int i=0;i<=Tag;i++) !check(0,i)?dp[0][i]=1:1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=Tag;j++){
			if(check(i,j)) continue;
			for(int k=0;k<=Tag;k++){
				if(j&k || check(i-1,k)) continue;
				dp[i][j]+=dp[i-1][k];
				dp[i][j]%=mod;
			}
		}
	for(int i=0;i<=Tag;i++) ans+=dp[n][i],ans%=mod;
	printf("%d",ans);
	return 0;
}

Solution

经典的棋盘状压,挺好玩。

观察数据范围,发现 m m m 很小,考虑状压。

可以看到一个炮兵只被上一排和上上一排影响。

那么我们我们的状态定义就很显然:

设 d p i , j , k dp_{i,j,k} dpi,j,k​ 表示第 i i i 行状态为 j j j,第 i − 1 i-1 i−1 行状态为 k k k 的情况。

然后直接枚举三行的情况,判断是否相邻。

看到这里相信大家都有了一点疑惑: 2 10 × 2 10 × 2 10 × 100 = 107374182400 2^{10}\times 2^{10}\times 2^{10}\times 100=107374182400 210×210×210×100=107374182400

岂不爆炸,实际上,这是一个状压很精髓的地方。

对于一行来讲,本事就有许多状态不合法,就如本身就相邻。

我算了一下,只有: 144 144 144 种情况

那么带进去就有: 298598400 298598400 298598400 的复杂度。

再加上枚举两个删掉一些,和图中本来有一些不能放,就可以排除大部分。

反正一句话:跑不满。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1e2+5;
const int M=1e1+1;
int n,m,Fi,ans;
int dp[N][1<<10][1<<10],Bit[1<<10];
bool Check[N][1<<M],f[11],k[N][M];
bool check(int i,int j){
	memset(f,0,sizeof(f));
	int x=1;
	while(j){
		f[x]=(j&1);
		j>>=1;
		x++;
	}
	for(int l=1;l<=m;l++) if(f[l] && !k[i][l]) return 0;
	return 1; 
}
int Count(int x){
	int sum=0;
	while(x) sum+=x&1,x>>=1;
	return sum;
}
int main(){
	scanf("%d %d",&n,&m);
	Fi=(1<<m)-1;
	for(register int i=1;i<=m;i++) k[0][i]=1;
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=m;j++){
			char opt;
			cin>>opt;
			if(opt=='P') k[i][j]=1;
		}
	for(register int i=0;i<=Fi;i++)
		for(register int j=0;j<=n;j++) (!check(j,i) || i&(i<<1) || i&(i<<2))?1:Check[j][i]=1;
	for(register int j1=0;j1<=Fi;j1++){
		Bit[j1]=Count(j1);
		if(Check[1][j1]) dp[1][j1][0]=Bit[j1]; 
	}	
	for(register int i=2;i<=n;i++)
		for(register int j1=0;j1<=Fi;j1++)
			if(Check[i][j1])
				for(register int j2=0;j2<=Fi;j2++)
					if(Check[i-1][j2] && !(j1&j2))
						for(register int j3=0;j3<=Fi;j3++)
							if(Check[i-2][j3])
								if(!(j1&j2) && !(j2&j3) && !(j1&j3)) dp[i][j1][j2]=max(dp[i-1][j2][j3]+Bit[j1],dp[i][j1][j2]);
	for(register int i=0;i<=Fi;i++)
		for(register int j=0;j<=Fi;j++) ans=max(dp[n][i][j],ans);
	printf("%d",ans);
	return 0;
}

Solution

感觉也很好玩。

我们知道,三点确定一个抛物线,这里已经给出了原点,即两点确定一条抛物线。

我们先把所有的抛物线整出来。

先推一下式子。

{ a ⋅ A x 2 + b ⋅ A x = A y a ⋅ B x 2 + b ⋅ B x = B y \begin{cases}a\cdot A_x^2+b\cdot A_x=A_y\\a\cdot B_x^2+b\cdot B_x=B_y\end{cases} {a⋅Ax2​+b⋅Ax​=Ay​a⋅Bx2​+b⋅Bx​=By​​

设参数 k = A x 2 B x 2 k=\frac{A_x^2}{B_x^2} k=Bx2​Ax2​​

第二个式子乘上参数:

a ⋅ A x 2 + k ⋅ b ⋅ B x = k ⋅ B y \begin{aligned} a\cdot A_x^2+k\cdot b\cdot B_x&=k\cdot B_y\end{aligned} a⋅Ax2​+k⋅b⋅Bx​​=k⋅By​​

让一式减去二式:

b ⋅ A x − k ⋅ b ⋅ B x = A y − k ⋅ B y b ⋅ ( A x − k ⋅ B x ) = A y − k ⋅ B y b = A y − k ⋅ B y A x − k ⋅ B x \begin{aligned}b\cdot A_x-k\cdot b\cdot B_x&=A_y-k\cdot B_y \\ b\cdot(A_x-k\cdot B_x) &=A_y-k\cdot B_y \\ b &=\frac{A_y-k\cdot B_y}{A_x-k\cdot B_x} \end{aligned} b⋅Ax​−k⋅b⋅Bx​b⋅(Ax​−k⋅Bx​)b​=Ay​−k⋅By​=Ay​−k⋅By​=Ax​−k⋅Bx​Ay​−k⋅By​​​

通过这样我们就把 b b b 给解出来了。

随便带进去,带到一式吧。

a ⋅ A x 2 = A y − b ⋅ A x a = A y − b ⋅ A x A x 2 \begin{aligned}a\cdot A_x^2 &= A_y-b\cdot A_x \\ a &= \frac{A_y-b\cdot A_x}{A_x^2} \end{aligned} a⋅Ax2​a​=Ay​−b⋅Ax​=Ax2​Ay​−b⋅Ax​​​

就这么解完了。

由于一条抛物线可能经过多个点,所以我们把每一条抛物线跑一遍,用二进制表示出其能经过哪些点。

然后问题就转化成了一个背包问题。

在这里,就把每个抛物线的价值进行了状压。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
int T,n,m,cnt;
int Fc[400],dp[1<<19];
struct node{
	double x,y;
}a[19];
struct Par{
	double a,b;
}f[400];
bool Check(int i,int j){
	return fabs(a[j].y-(f[i].a*a[j].x*a[j].x+f[i].b*a[j].x))<0.000001;
}
int main(){
	scanf("%d",&T);
	while(T--){
		cnt=0;
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++) scanf("%lf %lf",&a[i].x,&a[i].y);
		for(int i=1;i<=n;i++)
			for(int j=i+1;j<=n;j++){
				double k=(a[i].x*a[i].x)/(a[j].x*a[j].x),A,B;
				B=(a[i].y-k*a[j].y)/(a[i].x-k*a[j].x);
				A=(a[i].y-a[i].x*B)/(a[i].x*a[i].x);
				if(A<0) f[++cnt]=((Par){A,B});
			}
		mem(Fc,0);
		mem(dp,0x3f);
		dp[0]=0;
		int ans=0;
		for(int i=1;i<=cnt;i++){
			for(int j=1;j<=n;j++) Fc[i]=Fc[i]*2+Check(i,j);
		}
		for(int i=1;i<=n;i++) Fc[++cnt]=1<<i-1;
		for(int i=0;i<=(1<<n)-1;i++)
			for(int j=1;j<=cnt;j++) dp[i|Fc[j]]=min(dp[i|Fc[j]],dp[i]+1);
		printf("%d\n",dp[(1<<n)-1]);
	}
	return 0;
} 

Solution

感觉全部题最难的一道,最好玩的。

{ a 3 ⋅ a 9 ⋅ a . . . 2 ⋅ a 6 ⋅   a 18 ⋅ a . . . 4 ⋅ a 12 ⋅ a 36 ⋅ a . . . . . . . . . . . . . . . } \begin{Bmatrix}a & 3\cdot a & 9\cdot a & ...\\ 2\cdot a & 6\cdot\ a & 18\cdot a & ... \\ 4\cdot a & 12\cdot a & 36\cdot a & ...\\... &... &...&... \end{Bmatrix} ⎩⎪⎪⎨⎪⎪⎧​a2⋅a4⋅a...​3⋅a6⋅ a12⋅a...​9⋅a18⋅a36⋅a...​............​⎭⎪⎪⎬⎪⎪⎫​

通过观察这个矩阵我们发现,不能取这个矩阵相邻的元素作为集合。

那么这不禁让我们想起了第一题,而且对于矩阵的大小,是 log ⁡ \log log 级别的。

那么我们的方法就显而易见了。

对于这个矩阵,我们把所有没有出现过的元素标记上,这样就避免了重复的计算。

当我们算完了一个矩阵,需要进行相乘,因为肯定任意两个矩阵互相还是可以取,就是乘法原理。

万一矩阵的数量太多,跑飞了怎么办?

别慌,我们来想一想。

我跑了一下 1 0 5 10^5 105,跑了 33333 33333 33333 个矩阵。

那接下来我们算一下一个矩阵的复杂度。

约是 10000 10000 10000 级别的样子,但是跑不满,因此还是可以过的,况且随着矩阵越来越多,状态就越来越少,因为越来越多的数都会被标记。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
const int N=5e6+5;
const int mod=1e9+1;
int Fi,a[19][12],dp[18][5096];
int n,ans=1,Len[18],dq[18];
bool f[N],flag[18][12];
void Work(int x){
	mem(a,0);
	a[1][1]=x;
	int m=1,sum=0,len=0;
	for(int i=2;a[1][i-1]*3<=n;i++){
		a[1][i]=a[1][i-1]*3;
		m++;
	}
	for(int j=1;j<=m;j++)
		for(int i=2;a[i-1][j]*2<=n;i++) a[i][j]=a[i-1][j]*2;
	mem(Len,0);
	for(int i=1;a[i][1];i++){
		for(int j=1;j<=m;j++){
			Len[i]=Len[i]*2+(!a[i][j]);
			if(!a[i][j]) flag[i][j]=1;
			f[a[i][j]]=1;
		}
		len++;
	}
	mem(dp,0);
	dp[0][0]=1;
	for(int i=1;i<=len;i++){
		for(int j1=0;j1<=(1<<m)-1;j1++){
			if(Len[i]&j1 || j1&(j1<<1)) continue;
			for(int j2=0;j2<=(1<<m)-1;j2++){
				if(Len[i-1]&j2 || j2&(j2<<1) || j1&j2) continue;
				dp[i][j1]+=dp[i-1][j2];
				dp[i][j1]%=mod;
			}
		}
	}
	for(int i=0;i<=(1<<m)-1;i++) sum+=dp[len][i],sum%=mod;
	ans=(1ll*ans*sum)%mod;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		if(!f[i]){
			f[i]=1;
			Work(i);
		}
	}
	printf("%d",ans);
	return 0;
}

Solution

这题在洛谷上使用是标算二十倍的算法卡得过去。

Link

做得最爽的一道题。

首先还是思考如何将这个问题转化为状压。

其实这个题的连线和状压的另一种版题有点像,就是说传球,但是中间的直线需要另行判断。

什么直线呢?

就是说因为两点的直线必须是中间经过的点是使用过的。

可以很自然的想到直接枚举一层 n n n,然后暴力判断斜率。

O ( 2 n ⋅ n 3 ) O(2^n\cdot n^3) O(2n⋅n3)

8388608000 8388608000 8388608000,这么大,但是考虑到状压本身跑不满,因此复杂度虽然比这个小,但还是跑不过。

考虑位运算优化,我们先把任意两点间连线有的点给预处理下来。

然后进行状压取反后再和实际状态做与运算。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
const int mod=1e8+7;
int n,Fi;
int dp[20][1<<19];
int sum[1<<19],check[20][20];
bool f[20];
struct node{
	int x,y;
}a[20];
inline int qr(){
	int s=0,f=1;
	char ch;
	ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=s*10+ch-'0';ch=getchar();}
	return s*f;
}
inline void qw(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) qw(x/10);
	putchar(x%10+'0');
}
inline bool cmp(node x,node y){
	if(x.x==y.x) return x.y<y.y;
	return x.x<y.x;
}
inline void opt(int x){
	mem(f,0);
	int w=1;
	while(x) f[w]=(x&1),x>>=1,w++;
}
inline int Count(int x){
	int sum=0;
	while(x) sum+=x&1,x>>=1;
	return sum;
}
inline int Checker(int x,int y){
	mem(f,0);
	if(x>y) swap(x,y);
	int FF=0;
	if(x+1==y || x==y) return 0;
	for(int i=x+1;i<y;i++) if((a[x].x-a[i].x)*(a[i].y-a[y].y)==(a[i].x-a[y].x)*(a[x].y-a[i].y)) f[i]=1;
	for(int i=1;i<=n;i++) FF+=f[i]?1<<i-1:0;
	return FF;
}
int main(){
	n=qr();
	Fi=(1<<n)-1; 
	for(register int i=1;i<=n;i++) a[i]=((node){qr(),qr()});
	sort(a+1,a+1+n,cmp);
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=n;j++) check[i][j]=Checker(i,j);
	for(register int i=1;i<=n;i++) dp[i][1<<i-1]=1;
	for(register int j=0;j<=Fi;j++){
		opt(j);
		for(register int i=1;i<=n;i++){
			if(!f[i]) continue;
			for(register int k=1;k<=n;k++)
				if(f[k] && !(check[i][k]&(~j)) && k!=i) dp[i][j]+=dp[k][j-(1<<i-1)],dp[i][j]%=mod;
			sum[j]+=dp[i][j];
			sum[j]%=mod;
		}
	}
	int ans=0;
	for(register int j=0;j<=Fi;j++){
		if(Count(j)>=4) ans+=sum[j],ans%=mod;
	}
	printf("%d",ans);
	return 0;
} 
```# 前言

状压 DP 就是一类使用根据几进制来进行状态转移方程。

一般来说,数据范围 $2^n$ 都在情理之中,可是 $n!$ 就直接爆炸。

状压一般来说都可以用搜索,并且搜索都可以拿分,只是拿不全。

所以说,一般看见有一维是 $11$~$19$ 的话,那可以考虑状压。

# 题目选讲

* [P1879 ](https://www.luogu.com.cn/problem/P1879)

**Solution**

经典的状压 DP,算是入门题目吧。

我们发现 $n$ 和 $m$ 都特别小,但是搜索肯定过不了,那么我们思考一下状压。

我们发现:每一行的状态可以用一个二进制来表示,每一块草地种或者不种。

而且当前这块草地仅仅受到前一行草地的影响,也就是无后效性,肯定就是打 DP。

我们设 $dp_{i,j}$ 表示第 $i$ 行状态为 $j$ 的情况下,有多少种方案数。

这里,$j$ 表示出来是十进制数,实际上它表示了一种二进制下的一种状态。

首先排除这一行本身不合法的情况,即存在相邻或者不能种的草地上种了草。

对于这种判断,像我这种蒟蒻只会先用个数组存下来,然后在暴力判断,带一点常数。

大佬们可以尝试用位运算试试,~~只是很容易出锅。~~

接着我们枚举上一行,然后再看两行是否有相邻,如果合法了,那么累加就行了。

**Code**

```cpp
#include<bits/stdc++.h>
using namespace std;
const int N=13;
const int mod=1e8;
int n,m,ans;
int a[N][N],Log[N],Tag;
int dp[N][1<<N];
bool check(int i,int j){
	if(j&(j<<1)) return 1;
	int w=m;
	while(j){
		if(j&1 && !a[i][w]) return 1;
		j>>=1;
		w--;
	}
	return 0;
}
int main(){
	scanf("%d %d",&n,&m);
	Tag=1<<m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
	for(int i=0;i<=Tag;i++) !check(0,i)?dp[0][i]=1:1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=Tag;j++){
			if(check(i,j)) continue;
			for(int k=0;k<=Tag;k++){
				if(j&k || check(i-1,k)) continue;
				dp[i][j]+=dp[i-1][k];
				dp[i][j]%=mod;
			}
		}
	for(int i=0;i<=Tag;i++) ans+=dp[n][i],ans%=mod;
	printf("%d",ans);
	return 0;
}

Solution

经典的棋盘状压,挺好玩。

观察数据范围,发现 m m m 很小,考虑状压。

可以看到一个炮兵只被上一排和上上一排影响。

那么我们我们的状态定义就很显然:

设 d p i , j , k dp_{i,j,k} dpi,j,k​ 表示第 i i i 行状态为 j j j,第 i − 1 i-1 i−1 行状态为 k k k 的情况。

然后直接枚举三行的情况,判断是否相邻。

看到这里相信大家都有了一点疑惑: 2 10 × 2 10 × 2 10 × 100 = 107374182400 2^{10}\times 2^{10}\times 2^{10}\times 100=107374182400 210×210×210×100=107374182400

岂不爆炸,实际上,这是一个状压很精髓的地方。

对于一行来讲,本事就有许多状态不合法,就如本身就相邻。

我算了一下,只有: 144 144 144 种情况

那么带进去就有: 298598400 298598400 298598400 的复杂度。

再加上枚举两个删掉一些,和图中本来有一些不能放,就可以排除大部分。

反正一句话:跑不满。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1e2+5;
const int M=1e1+1;
int n,m,Fi,ans;
int dp[N][1<<10][1<<10],Bit[1<<10];
bool Check[N][1<<M],f[11],k[N][M];
bool check(int i,int j){
	memset(f,0,sizeof(f));
	int x=1;
	while(j){
		f[x]=(j&1);
		j>>=1;
		x++;
	}
	for(int l=1;l<=m;l++) if(f[l] && !k[i][l]) return 0;
	return 1; 
}
int Count(int x){
	int sum=0;
	while(x) sum+=x&1,x>>=1;
	return sum;
}
int main(){
	scanf("%d %d",&n,&m);
	Fi=(1<<m)-1;
	for(register int i=1;i<=m;i++) k[0][i]=1;
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=m;j++){
			char opt;
			cin>>opt;
			if(opt=='P') k[i][j]=1;
		}
	for(register int i=0;i<=Fi;i++)
		for(register int j=0;j<=n;j++) (!check(j,i) || i&(i<<1) || i&(i<<2))?1:Check[j][i]=1;
	for(register int j1=0;j1<=Fi;j1++){
		Bit[j1]=Count(j1);
		if(Check[1][j1]) dp[1][j1][0]=Bit[j1]; 
	}	
	for(register int i=2;i<=n;i++)
		for(register int j1=0;j1<=Fi;j1++)
			if(Check[i][j1])
				for(register int j2=0;j2<=Fi;j2++)
					if(Check[i-1][j2] && !(j1&j2))
						for(register int j3=0;j3<=Fi;j3++)
							if(Check[i-2][j3])
								if(!(j1&j2) && !(j2&j3) && !(j1&j3)) dp[i][j1][j2]=max(dp[i-1][j2][j3]+Bit[j1],dp[i][j1][j2]);
	for(register int i=0;i<=Fi;i++)
		for(register int j=0;j<=Fi;j++) ans=max(dp[n][i][j],ans);
	printf("%d",ans);
	return 0;
}

Solution

感觉也很好玩。

我们知道,三点确定一个抛物线,这里已经给出了原点,即两点确定一条抛物线。

我们先把所有的抛物线整出来。

先推一下式子。

{ a ⋅ A x 2 + b ⋅ A x = A y a ⋅ B x 2 + b ⋅ B x = B y \begin{cases}a\cdot A_x^2+b\cdot A_x=A_y\\a\cdot B_x^2+b\cdot B_x=B_y\end{cases} {a⋅Ax2​+b⋅Ax​=Ay​a⋅Bx2​+b⋅Bx​=By​​

设参数 k = A x 2 B x 2 k=\frac{A_x^2}{B_x^2} k=Bx2​Ax2​​

第二个式子乘上参数:

a ⋅ A x 2 + k ⋅ b ⋅ B x = k ⋅ B y \begin{aligned} a\cdot A_x^2+k\cdot b\cdot B_x&=k\cdot B_y\end{aligned} a⋅Ax2​+k⋅b⋅Bx​​=k⋅By​​

让一式减去二式:

b ⋅ A x − k ⋅ b ⋅ B x = A y − k ⋅ B y b ⋅ ( A x − k ⋅ B x ) = A y − k ⋅ B y b = A y − k ⋅ B y A x − k ⋅ B x \begin{aligned}b\cdot A_x-k\cdot b\cdot B_x&=A_y-k\cdot B_y \\ b\cdot(A_x-k\cdot B_x) &=A_y-k\cdot B_y \\ b &=\frac{A_y-k\cdot B_y}{A_x-k\cdot B_x} \end{aligned} b⋅Ax​−k⋅b⋅Bx​b⋅(Ax​−k⋅Bx​)b​=Ay​−k⋅By​=Ay​−k⋅By​=Ax​−k⋅Bx​Ay​−k⋅By​​​

通过这样我们就把 b b b 给解出来了。

随便带进去,带到一式吧。

a ⋅ A x 2 = A y − b ⋅ A x a = A y − b ⋅ A x A x 2 \begin{aligned}a\cdot A_x^2 &= A_y-b\cdot A_x \\ a &= \frac{A_y-b\cdot A_x}{A_x^2} \end{aligned} a⋅Ax2​a​=Ay​−b⋅Ax​=Ax2​Ay​−b⋅Ax​​​

就这么解完了。

由于一条抛物线可能经过多个点,所以我们把每一条抛物线跑一遍,用二进制表示出其能经过哪些点。

然后问题就转化成了一个背包问题。

在这里,就把每个抛物线的价值进行了状压。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
int T,n,m,cnt;
int Fc[400],dp[1<<19];
struct node{
	double x,y;
}a[19];
struct Par{
	double a,b;
}f[400];
bool Check(int i,int j){
	return fabs(a[j].y-(f[i].a*a[j].x*a[j].x+f[i].b*a[j].x))<0.000001;
}
int main(){
	scanf("%d",&T);
	while(T--){
		cnt=0;
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++) scanf("%lf %lf",&a[i].x,&a[i].y);
		for(int i=1;i<=n;i++)
			for(int j=i+1;j<=n;j++){
				double k=(a[i].x*a[i].x)/(a[j].x*a[j].x),A,B;
				B=(a[i].y-k*a[j].y)/(a[i].x-k*a[j].x);
				A=(a[i].y-a[i].x*B)/(a[i].x*a[i].x);
				if(A<0) f[++cnt]=((Par){A,B});
			}
		mem(Fc,0);
		mem(dp,0x3f);
		dp[0]=0;
		int ans=0;
		for(int i=1;i<=cnt;i++){
			for(int j=1;j<=n;j++) Fc[i]=Fc[i]*2+Check(i,j);
		}
		for(int i=1;i<=n;i++) Fc[++cnt]=1<<i-1;
		for(int i=0;i<=(1<<n)-1;i++)
			for(int j=1;j<=cnt;j++) dp[i|Fc[j]]=min(dp[i|Fc[j]],dp[i]+1);
		printf("%d\n",dp[(1<<n)-1]);
	}
	return 0;
} 

Solution

感觉全部题最难的一道,最好玩的。

{ a 3 ⋅ a 9 ⋅ a . . . 2 ⋅ a 6 ⋅   a 18 ⋅ a . . . 4 ⋅ a 12 ⋅ a 36 ⋅ a . . . . . . . . . . . . . . . } \begin{Bmatrix}a & 3\cdot a & 9\cdot a & ...\\ 2\cdot a & 6\cdot\ a & 18\cdot a & ... \\ 4\cdot a & 12\cdot a & 36\cdot a & ...\\... &... &...&... \end{Bmatrix} ⎩⎪⎪⎨⎪⎪⎧​a2⋅a4⋅a...​3⋅a6⋅ a12⋅a...​9⋅a18⋅a36⋅a...​............​⎭⎪⎪⎬⎪⎪⎫​

通过观察这个矩阵我们发现,不能取这个矩阵相邻的元素作为集合。

那么这不禁让我们想起了第一题,而且对于矩阵的大小,是 log ⁡ \log log 级别的。

那么我们的方法就显而易见了。

对于这个矩阵,我们把所有没有出现过的元素标记上,这样就避免了重复的计算。

当我们算完了一个矩阵,需要进行相乘,因为肯定任意两个矩阵互相还是可以取,就是乘法原理。

万一矩阵的数量太多,跑飞了怎么办?

别慌,我们来想一想。

我跑了一下 1 0 5 10^5 105,跑了 33333 33333 33333 个矩阵。

那接下来我们算一下一个矩阵的复杂度。

约是 10000 10000 10000 级别的样子,但是跑不满,因此还是可以过的,况且随着矩阵越来越多,状态就越来越少,因为越来越多的数都会被标记。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
const int N=5e6+5;
const int mod=1e9+1;
int Fi,a[19][12],dp[18][5096];
int n,ans=1,Len[18],dq[18];
bool f[N],flag[18][12];
void Work(int x){
	mem(a,0);
	a[1][1]=x;
	int m=1,sum=0,len=0;
	for(int i=2;a[1][i-1]*3<=n;i++){
		a[1][i]=a[1][i-1]*3;
		m++;
	}
	for(int j=1;j<=m;j++)
		for(int i=2;a[i-1][j]*2<=n;i++) a[i][j]=a[i-1][j]*2;
	mem(Len,0);
	for(int i=1;a[i][1];i++){
		for(int j=1;j<=m;j++){
			Len[i]=Len[i]*2+(!a[i][j]);
			if(!a[i][j]) flag[i][j]=1;
			f[a[i][j]]=1;
		}
		len++;
	}
	mem(dp,0);
	dp[0][0]=1;
	for(int i=1;i<=len;i++){
		for(int j1=0;j1<=(1<<m)-1;j1++){
			if(Len[i]&j1 || j1&(j1<<1)) continue;
			for(int j2=0;j2<=(1<<m)-1;j2++){
				if(Len[i-1]&j2 || j2&(j2<<1) || j1&j2) continue;
				dp[i][j1]+=dp[i-1][j2];
				dp[i][j1]%=mod;
			}
		}
	}
	for(int i=0;i<=(1<<m)-1;i++) sum+=dp[len][i],sum%=mod;
	ans=(1ll*ans*sum)%mod;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		if(!f[i]){
			f[i]=1;
			Work(i);
		}
	}
	printf("%d",ans);
	return 0;
}

Solution

这题在洛谷上使用是标算二十倍的算法卡得过去。

Link

做得最爽的一道题。

首先还是思考如何将这个问题转化为状压。

其实这个题的连线和状压的另一种版题有点像,就是说传球,但是中间的直线需要另行判断。

什么直线呢?

就是说因为两点的直线必须是中间经过的点是使用过的。

可以很自然的想到直接枚举一层 n n n,然后暴力判断斜率。

O ( 2 n ⋅ n 3 ) O(2^n\cdot n^3) O(2n⋅n3)

8388608000 8388608000 8388608000,这么大,但是考虑到状压本身跑不满,因此复杂度虽然比这个小,但还是跑不过。

考虑位运算优化,我们先把任意两点间连线有的点给预处理下来。

然后进行状压取反后再和实际状态做与运算。

Code

#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
using namespace std;
const int mod=1e8+7;
int n,Fi;
int dp[20][1<<19];
int sum[1<<19],check[20][20];
bool f[20];
struct node{
	int x,y;
}a[20];
inline int qr(){
	int s=0,f=1;
	char ch;
	ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=s*10+ch-'0';ch=getchar();}
	return s*f;
}
inline void qw(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) qw(x/10);
	putchar(x%10+'0');
}
inline bool cmp(node x,node y){
	if(x.x==y.x) return x.y<y.y;
	return x.x<y.x;
}
inline void opt(int x){
	mem(f,0);
	int w=1;
	while(x) f[w]=(x&1),x>>=1,w++;
}
inline int Count(int x){
	int sum=0;
	while(x) sum+=x&1,x>>=1;
	return sum;
}
inline int Checker(int x,int y){
	mem(f,0);
	if(x>y) swap(x,y);
	int FF=0;
	if(x+1==y || x==y) return 0;
	for(int i=x+1;i<y;i++) if((a[x].x-a[i].x)*(a[i].y-a[y].y)==(a[i].x-a[y].x)*(a[x].y-a[i].y)) f[i]=1;
	for(int i=1;i<=n;i++) FF+=f[i]?1<<i-1:0;
	return FF;
}
int main(){
	n=qr();
	Fi=(1<<n)-1; 
	for(register int i=1;i<=n;i++) a[i]=((node){qr(),qr()});
	sort(a+1,a+1+n,cmp);
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=n;j++) check[i][j]=Checker(i,j);
	for(register int i=1;i<=n;i++) dp[i][1<<i-1]=1;
	for(register int j=0;j<=Fi;j++){
		opt(j);
		for(register int i=1;i<=n;i++){
			if(!f[i]) continue;
			for(register int k=1;k<=n;k++)
				if(f[k] && !(check[i][k]&(~j)) && k!=i) dp[i][j]+=dp[k][j-(1<<i-1)],dp[i][j]%=mod;
			sum[j]+=dp[i][j];
			sum[j]%=mod;
		}
	}
	int ans=0;
	for(register int j=0;j<=Fi;j++){
		if(Count(j)>=4) ans+=sum[j],ans%=mod;
	}
	printf("%d",ans);
	return 0;
} 
上一篇:【SLAM基础】【矩阵】矩阵基础相关概念总结


下一篇:Windows下socket编程(console非MFC)