[UVA1602]Lattice Animals

280 篇文章 1 订阅
7 篇文章 0 订阅

题目

传送门 to UVA

题目描述
有多少个本质不同的四连通块,恰包含 n n n 个格子,且能够放入 h × w h\times w h×w 的矩形?如果两个四连通块可以通过翻转、旋转、平移变得相同,那么二者本质相同。

数据范围与提示
max ⁡ ( n , h , w ) ⩽ 10 \max(n,h,w)\leqslant 10 max(n,h,w)10

思路

塔木德,这题神了!

发现一共只有 1 0 3 10^3 103 种不同的输入,并且题目看上去不可做的样子,肯定考虑打表。不过 ( 100 10 ) {100\choose 10} (10100) 也搜不出来啊……

此时,我旁边坐着一条吃得翔中翔,方为狗上狗!它说:“汪汪,汪旺妄,往往忘网,枉王!”(由 G o o g l a \rm Googla Googla 提供狗语翻译:其实它很快答案就是零了!)

什么叫 “很快” ?就是 h + w > n + 1 h+w>n+1 h+w>n+1 的时候。当然这里需要 恰好占用一个 h × w h\times w h×w 的子矩形。更强的限制条件,只会让搜索更快,这就是剪枝简单而强大的原理!

所以实际上只会是 5 × 6 5\times 6 5×6 4 × 7 4\times 7 4×7 等的子矩形。那么只有最多 ( 35 10 ) {35\choose 10} (1035) 个情况。多数情况下它还不会形成连通块,其实非常快!

当然,怎么判断本质相同?就用 B u r n s i d e \rm Burnside Burnside 定理 也是可以的。注意恰好占用 h × w h\times w h×w 的子矩形,和恰好占用 w × h w\times h w×h 的子矩形,二者之间是本质相同的。做 B u r n s i d e \rm Burnside Burnside 要注意,一定要构成群,所以要二者一起考虑。最后统计答案的时候,也只加其中之一。

具体怎么打呢?注意到旋转和翻转本质都是对向量 ( x , y ) (x,y) (x,y) 做线性变换。要么是在某一维加负号,要么是交换两个维度,并在其中一维添符号。所以最终 ( x , y ) (x,y) (x,y) 一定得到 ( ± x , ± y ) (\pm x,\pm y) (±x,±y) ( ± y , ± x ) (\pm y,\pm x) (±y,±x)

注意还有平移。平移不改变相对位置关系。找到原图形的最靠下、最靠左的一个。那么根据变换规则,很容易得到它应该对应到新图形的哪个点(最靠左或右,最靠上或下),就推出了变换后的平移量。

试了试,只跑了 5 s 5s 5s 左右就可以输出整张表。不可思议!

代码

当然是给出打表程序。注意双重循环用一个 b r e a k \tt break break 不能弹出干净!

如果 h = w h=w h=w,那就是有 8 8 8 种置换;否则要先乘 2 2 2,得到 f ( w , h ) + f ( h , w ) f(w,h)+f(h,w) f(w,h)+f(h,w) 的值,再除以 8 8 8

#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int_ readint(){
	int_ a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}

const int MaxN = 10;
int a[MaxN][MaxN], w, h, n;

int ans, mat[2][2], cnt;
bool vis[MaxN][MaxN];
void dfs(int x,int y){
	if(x < 0 || x >= w) return ;
	if(y < 0 || y >= h) return ;
	if(!a[x][y] || vis[x][y]) return ;
	vis[x][y] = true, ++ cnt;
	dfs(x,y-1), dfs(x,y+1);
	dfs(x-1,y), dfs(x+1,y);
}
bool checkMove(int tx,int ty){
	int vx = mat[0][0]+mat[1][0], dx = 0;
	int vy = mat[0][1]+mat[1][1], dy = 0;
	int frx = 0, tox = w; // [from,to)
	if(vx == -1) // (-y,?) or (-x,?)
		frx = w-1, tox = -1;
	int fry = 0, toy = h;
	if(vy == -1) // (?,-x) or (?,-y)
		fry = h-1, toy = -1;
	if(mat[0][0]){ // no swapping
		for(int i=frx; i!=tox; i+=vx)
		for(int j=fry; j!=toy; j+=vy){
			if(!a[i][j]) continue;
			dx = i-(tx*mat[0][0]);
			dy = j-(ty*mat[1][1]);
			goto FUCK_DOUBLE_FOR_LOOP;
		}
	}
	else{ // swapped coordinate
		for(int j=fry; j!=toy; j+=vy)
		for(int i=frx; i!=tox; i+=vx){
			if(!a[i][j]) continue;
			dx = i-(ty*mat[1][0]);
			dy = j-(tx*mat[0][1]);
			goto FUCK_DOUBLE_FOR_LOOP;
		}
	}
	FUCK_DOUBLE_FOR_LOOP:
	rep(i,0,w-1) rep(j,0,h-1){
		if(!a[i][j]) continue;
		int I = i*mat[0][0]+j*mat[1][0]+dx;
		int J = i*mat[0][1]+j*mat[1][1]+dy;
		if(I < 0 or I >= w or
		J < 0 or J >= h or a[I][J] == 0)
			return false;
	}
	return true;
}
void check(){
	bool f_l = false, f_r = false;
	rep(i,0,w-1){
		memset(vis[i],0,h);
		if(a[i][0]) f_l = true;
		if(a[i][h-1]) f_r = 1;
	}
	if(!f_l || !f_r) return ;
	rep(j,f_r=0,h-1)
		if(a[w-1][j]) f_r = 1;
	if(!f_r) return ;
	int tx, ty; // save position
	rep(j,f_l=0,h-1){
		if(!a[0][j]) continue;
		f_l = 1, cnt = 0;
		dfs(tx = 0, ty = j);
		if(cnt != n) return ;
		else break;
	}
	if(!f_l) return ; // not upmost
	rep(i,0,1) rep(j,0,1) rep(k,0,1){
		mat[0][i] = 1-(j<<1);
		mat[1][i^1] = 1-(k<<1);
		mat[0][i^1] = mat[1][i] = 0;
		ans += checkMove(tx,ty);
	}
}
void solve(int t=0,int cnt=0){
	if(cnt == n) return check();
	if(t == h && !cnt) return ; // pre-check
	if(t == h*w) return ;
	a[t/h][t%h] = 1, solve(t+1,cnt+1);
	a[t/h][t%h] = 0, solve(t+1,cnt);
}

int main(){
	// freopen("AC.cpp","w",stdout);
	for(n=1; n<=10; ++n)
	for(h=1; h<=n; ++h)
	for(w=h; w<=n&&h+w<=n+1; ++w){
		ans = 0; solve();
		if(!ans) continue;
		printf("dp[%d][%d][%d] = %d;\n",n,h,w,ans>>2>>(h==w));
	}
	return 0;
}

以及注意答案输出的时候,不妨设 w ⩽ h w\leqslant h wh,那么只考虑 w ′ ⩽ h ′ w'\leqslant h' wh 的情况。核心代码见下。

	if(w > h) swap(w,h);
	rep(i,1,w) rep(j,i,h)
		ans += dp[n][i][j];
	printf("%d\n",ans);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一道比较经典的计数问题。题目描述如下: 给定一个 $n \times n$ 的网格图,其中一些格子被标记为障碍。一个连通块是指一些被标记为障碍的格子的集合,满足这些格子在网格图中连通。一个格子是连通的当且仅当它与另一个被标记为障碍的格子在网格图中有公共边。 现在,你需要计算在这个网格图中,有多少个不同的连通块,满足这个连通块的大小(即包含的格子数)恰好为 $k$。 这是一道比较经典的计数问题,一般可以通过计算生成函数的方法来解决。具体来说,我们可以定义一个生成函数 $F(x)$,其中 $[x^k]F(x)$ 表示大小为 $k$ 的连通块的个数。那么,我们可以考虑如何计算这个生成函数。 对于一个大小为 $k$ 的连通块,我们可以考虑它的形状。具体来说,我们可以考虑以该连通块的最左边、最上边的格子为起点,从上到下、从左到右遍历该连通块,把每个格子在该连通块中的相对位置记录下来。由于该连通块的大小为 $k$,因此这些相对位置一定是 $(x,y) \in [0,n-1]^2$ 中的 $k$ 个不同点。 现在,我们需要考虑如何计算这些点对应的连通块是否合法。具体来说,我们可以考虑从左到右、从上到下依次处理这些点,对于每个点 $(x,y)$,我们需要考虑它是否能够与左边的点和上边的点连通。具体来说,如果 $(x-1,y)$ 和 $(x,y)$ 都在该连通块中且它们在网格图中有公共边,那么它们就是连通的;同样,如果 $(x,y-1)$ 和 $(x,y)$ 都在该连通块中且它们在网格图中有公共边,那么它们也是连通的。如果 $(x,y)$ 与左边和上边的点都不连通,那么说明这个点不属于该连通块。 考虑到每个点最多只有两个方向需要检查,因此时间复杂度为 $O(n^2 k)$。不过,我们可以使用类似于矩阵乘法的思想,将这个过程优化到 $O(k^3)$ 的时间复杂度。 具体来说,我们可以设 $f_{i,j,k}$ 表示状态 $(i,j)$ 所代表的点在连通块中,且连通块的大小为 $k$ 的方案数。显然,对于一个合法的 $(i,j,k)$,我们可以考虑 $(i-1,j,k-1)$ 和 $(i,j-1,k-1)$ 这两个状态,然后把点 $(i,j)$ 加入到它们所代表的连通块中。因此,我们可以设计一个 $O(k^3)$ 的 DP 状态转移,计算 $f_{i,j,k}$。 具体来说,我们可以考虑枚举连通块所包含的最右边和最下边的格子的坐标 $(x,y)$,然后计算 $f_{x,y,k}$。对于一个合法的 $(x,y,k)$,我们可以考虑将 $(x,y)$ 所代表的点加入到 $(x-1,y,k-1)$ 和 $(x,y-1,k-1)$ 所代表的连通块中。不过,这里需要注意一个细节:如果 $(x-1,y)$ 和 $(x,y)$ 在网格图中没有相邻边,那么它们不能算作连通的。因此,我们需要特判这个情况。 最终,$f_{n,n,k}$ 就是大小为 $k$ 的连通块的个数,时间复杂度为 $O(n^2 k + k^3)$。 参考代码:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值