POJ1185炮兵阵地

题目描述

  司令部的将军们打算在 N × M N\times M N×M的网格地图上部署他们的炮兵部队。一个 N × M N\times M N×M的地图由 N N N M M M列组成,地图的每一格可能是山地(用’H’ 表示),也可能是平原(用’P’表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

  如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
  现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入

  第一行包含两个由空格分割开的正整数,分别表示 N N N M M M.
  接下来的N行,每一行含有连续的 M M M个字符(‘P’或者’H’),中间没有空格。按顺序表示地图中每一行的数据。 N ≤ 100 , M ≤ 10 N\leq 100,M\leq 10 N100,M10.
e . g .   I N P U T \mathrm{e.g.\ INPUT} e.g. INPUT

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

输出

  仅一行,包含一个整数 K K K,表示最多能摆放的炮兵部队的数量。
e . g .   O U T P U T \mathrm{e.g.\ OUTPUT} e.g. OUTPUT

6

算法设计

  采用动态规划的方式对该问题求解,而其核心就是以行为单位进行递推。
  该怎么存储地形情况呢?这里笔者不采用二维数组保存每一小块地的情况,而是采用一个一维数组fstate,其中fstate[i]表示第i行的土地状态。fstate[i]在数值上等于一个二进制串 f 0 f 1 ⋯ f M − 1 f_0f_1\cdots f_{M-1} f0f1fM1的值。对于地 i i i行的这 M M M块地而言,第 j j j块如果是平原,那么 f j = 1 f_j=1 fj=1,否则 f j = 0 f_j=0 fj=0.据此计算fstate[i]即可。代码如下:

for(int i=0;i<n;i++){
		for(int j=0;j<m;j++)
			if(getchar()=='P')
				fstate[i]=(fstate[i]<<1)+1;
			else
				fstate[i]<<=1;	
		getchar();
	}//地形初始化输入完成,1表示平原可以放置炮兵,0表示山地不能放置炮兵。

  再考虑放置炮兵。先不考虑限制条件。对于某一行而言,它的第 i i i块地有两种选择:放置炮兵(记作 s i = 1 s_i=1 si=1)和不放置炮兵(记作 s i = 0 s_i=0 si=0)。每一块地都是这样的情况,我们记二进制串 s 0 s 1 ⋯ s M − 1 s_0s_1\cdots s_{M-1} s0s1sM1为这一行炮兵放置的状态。显然,每个状态唯一地对应一个 M M M位二进制数,每一行都有 2 M 2^M 2M种可能状态。
  至此,我们就已经完成了状态压缩的任务——把每一行放置炮兵的状态,压缩成一个 M M M位二进制数。
  但实际上,放置炮兵有一些列的限制条件,所以在上面的 2 M 2^M 2M种状态中,有一些是显然不可能的。如果我们只考虑某一行(不考虑地形和上、下行的影响),会发现炮兵放置的限制只有“不能攻击到对方”这一条。这可以筛选掉 2 M 2^M 2M种状态中很多的无效状态。

  比如,如果某一行的状态是 s = 100 1 0 1 0 s=100\textcolor{red}10\textcolor{red}10 s=1001010,那么第 3 3 3块地上的炮兵与第 5 5 5块地上的炮兵就会产生相互攻击的情况。实际上,判断是否会相互攻击,就等同于判断状态串 s s s中,某个 1 1 1左侧两位的范围内是否也存在 1 1 1.如果存在,就有相互攻击的情况。
  s & (s<<1) 可以判断,状态串 s s s中,是否有某个 1 1 1的左侧第一位是 1 1 1.而s & (s<<2)则可以判断,状态串 s s s中,是否有某个 1 1 1的左侧第二位是 1 1 1.将这两个结果按位或以下(逻辑或也可)就能够判断状态 s s s是否存在相互攻击的状态。

  笔者亲自实践后,证明这一步筛选是很有必要的,否则会TLE。筛选采用下面的代码段进行:

for(int i=0;i<(1<<m);i++)//遍历2^m种状态
	if(!(i & (i<<1) | i & (i<<2))){
		srefl[state_cnt]=i;//状态i是符合“不能攻击到对方”这一条件的。
		for(int j=i;j;j/=2)
			nrefl[state_cnt]+=j%2;
		state_cnt++;
	}

  数组srefl是"state reflection"的缩写,意为“状态映射”,它存储了根据“不能攻击到对方”这一条件所筛选出来的所有有效状态。数组nrefl是“number reflection”的缩写,意为“数字映射”,存储了相应二进制状态中“1”的个数,也就对应该状态下该行放置的炮兵个数。

  srefl[i]表示第i个被找出来的有效状态 s i s_i si的值,nrefl[i]表示 s i s _i si 1 1 1的个数。

  接下来我们构建一个三维数组dp[i][j][k],其含义为当第i行炮兵放置为状态srefl[j]且第i-1行炮兵放置为状态srefl[k]时,前i行最多能够放置的炮兵数量。最优子结构性的证明我们在此省略,下面给出状态转移方程:
d p [ i ] [ j ] [ k ] = { max ⁡ l { d p [ i − 1 ] [ k ] [ l ] + n r e f l [ j ] } i f   ( j , k , l )   i s   c o m p a t i b l e , 0 o t h e r w i s e . (1) \mathrm{dp}[i][j][k]=\begin{cases}\max\limits _l\{\mathrm{dp}[i-1][k][l]+\mathrm{nrefl}[j]\}& \mathrm{if\ }(j,k,l)\mathrm{\ is\ compatible},\\ 0&\mathrm{otherwise}.\end{cases}\tag{1} dp[i][j][k]={lmax{dp[i1][k][l]+nrefl[j]}0if (j,k,l) is compatible,otherwise.(1)

  其中,兼容(compatible)的条件是第 i , i − 1 , i − 2 i,i-1,i-2 i,i1,i2行的炮兵纵向不能相互攻击到,也就是没有哪两个炮兵在同一列上。如果兼容,那么在前 i i i行最大炮兵的数量,等于前 i − 1 i-1 i1行最大炮兵数量加上第 i i i行的炮兵数 n r e f l [ j ] \mathrm{nrefl}[j] nrefl[j];否则,无法在这种状态下合法地放置炮兵,最大炮兵数为 0 0 0。根据状态的二进制表示,兼容这一条件也可以表述为第的状态两两按位与的结果为 0 0 0

srefl[j] & srefl[k] | srefl[k] & srefl[l] | srefl[j] & srefl[l] == 0

  假设第 i i i行与第 i − 1 i-1 i1行的某一位(比如第 x x x位)都有炮兵,那么srefl[j](对应第 i i i行的状态)和srefl[k](对应第 i − 1 i-1 i1行的状态)的二进制表示中,第 x x x位都是 1 1 1,它们的按位与srefl[j] & srefl[k]就非零。同样地,上面的代码块也可以用逻辑或(||)代替按位或(|)。

  由于 i = 0 i=0 i=0时没有第 i − 1 , i − 2 i-1,i-2 i1,i2行, i = 1 i=1 i=1时没有第 i − 2 i-2 i2行,所以我们对于 i = 0 , 1 i=0,1 i=0,1的情况特殊处理一下即可。自底向上的动态规划结束之后,答案就是state数组中第一维为n-1的元素中最大的那个:
a n s = max ⁡ 0 ≤ i < s t a t e _ c n t 0 ≤ j < s t a t e _ c n t { r e f l [ n − 1 ] [ i ] [ j ] } (2) \mathrm{ans}=\max\limits_{0\leq i<\mathrm{state\_cnt} \atop 0\leq j<\mathrm{state\_cnt}}\{\mathrm{refl}[n-1][i][j]\}\tag{2} ans=0j<state_cnt0i<state_cntmax{refl[n1][i][j]}(2)

AC代码

# include <iostream>
# include <stdlib.h>
# define STATESIZE 100 //STATESIZE应该要大于state_cnt,如果运行时RE了,就扩大STATESIZE。
int n,m,state_cnt=0;
int srefl[STATESIZE];
int nrefl[STATESIZE];
int dp[100][STATESIZE][STATESIZE],fstate[100]={0};//fstate储存每一块地的地形数据.
int main(){
	int ans=0;
	scanf(%d%d”,&n,&m);
	getchar();//读取回车符
	for(int i=0;i<n;i++){
		for(int j=0;j<m;j++)
			if(getchar()=='P')
				fstate[i]=(fstate[i]<<1)+1;
			else
				fstate[i]<<=1;	
		getchar();
	}
	for(int i=0;i<(1<<m);i++)
		if(!(i & (i<<1) | i & (i<<2) )){
			srefl[state_cnt]=i;//状态i是符合“不能攻击到对方”这一条件的。
			for(int j=i;j;j/=2)
				nrefl[state_cnt]+=j%2;
			state_cnt++;
		}
	for(int i=0;i<n;i++)//遍历每一行
		for(int j=0;j<state_cnt;j++)//遍历每一个合法状态
			if((srefl[j] & fstate[i])==srefl[j]){//判断地形条件是否能够放置状态为srefl[j]的炮兵
				if(i==0)//特判
					dp[i][j][0]=nrefl[j];
				else if(i==1){//特判
					for(int k=0;k<state_cnt;k++)
						if(!(srefl[j] & srefl[k]) && dp[i][j][k]<nrefl[j]+dp[i-1][k][0])//如果这三行是“兼容”的
							dp[i][j][k]=nrefl[j]+dp[i-1][k][0];
				}
				else{
					for(int k=0;k<state_cnt;k++)
						for(int l=0;l<state_cnt;l++)
							if(!(srefl[j] & srefl[k] | srefl[k] & srefl[l] | srefl[j] & srefl[l]) && dp[i][j][k]<nrefl[j]+dp[i-1][k][l])
								dp[i][j][k]=nrefl[j]+dp[i-1][k][l];
				}
			}	
	for(int i=0;i<state_cnt;i++)
		for(int j=0;j<state_cnt;j++)
			if(ans<dp[n-1][i][j])
				ans=dp[n-1][i][j];
	printf(%d”,ans);
	return 0;
}

时间复杂度分析

  输入地形部分,两个for循环,复杂度为 O ( m n ) O(mn) O(mn).但是,初始化srefl数组的过程,其时间复杂度是指数级的 O ( m ⋅ 2 m ) O(m\cdot 2^m) O(m2m),这个很大程度上影响着整个算法的复杂度。
  动态规划自底向上的过程用到了含state_cnt的循环,复杂度为 O ( n ⋅ s t a t e _ c n t 3 ) O(n\cdot \mathrm{state\_cnt}^3) O(nstate_cnt3).打印答案的时间复杂度为 O ( s t a t e _ c n t 2 ) . O(\mathrm{state\_cnt}^2). O(state_cnt2).
  据此,可以初步判断算法的时间复杂度为:
O ( m ⋅ 2 m + n ⋅ s t a t e _ c n t 3 ) (3) O(m\cdot 2^m+n\cdot\mathrm{state\_cnt}^3)\tag{3} O(m2m+nstate_cnt3)(3)

  那么这个 s t a t e _ c n t \mathrm{state\_cnt} state_cnt到底随 m , n m,n m,n是怎么变化的呢?显然它是我们考虑一行的情况判断的,所以 s t a t e _ c n t \mathrm{state\_cnt} state_cnt n n n无关。
  记 s t a t e _ c n t = T ( m ) \mathrm{state\_cnt}=T(m) state_cnt=T(m),可以得到以下递推方程。对于 T ( m ) T(m) T(m),如果第 0 0 0块地不放置炮兵,那么剩下的 m − 1 m-1 m1块地有 T ( m − 1 ) T(m-1) T(m1)中放置炮兵的方式;如果第 0 0 0块地放置炮兵,那么第 1 , 2 1,2 1,2块地都不允许放置炮兵,所以剩下的 m − 1 m-1 m1块第有 T ( n − 3 ) T(n-3) T(n3)种放置炮兵的方式。可以得到以下递推方程:
T ( m ) = T ( m − 1 ) + T ( m − 3 ) (4) T(m)=T(m-1)+T(m-3)\tag{4} T(m)=T(m1)+T(m3)(4)

  找出初值条件:
T ( 0 ) = 1 , T ( 1 ) = 2 , T ( 2 ) = 3 , T ( 3 ) = 4 ⋯ (5) T(0)=1,T(1)=2,T(2)=3,T(3)=4\cdots\tag{5} T(0)=1,T(1)=2,T(2)=3,T(3)=4(5)

  注意不放置炮兵也是一种可行的方案。

  递推方程 ( 4 ) (4) (4)的特征方程为:
λ 3 − λ 2 − 1 = 0 (6) \lambda^3-\lambda^2-1=0\tag{6} λ3λ21=0(6)

  分析函数 f ( λ ) = λ 3 − λ 2 − 1 f(\lambda)=\lambda^3-\lambda^2-1 f(λ)=λ3λ21可知,方程 ( 6 ) (6) (6) R \mathbb R R上有且只有一个实根 λ 0 \lambda_0 λ0.它大约是 1.466 1.466 1.466( λ 0 < 1.466 \lambda_0 <1.466 λ0<1.466).同时还有两个共轭复根 λ 1 , 2 = a ± b i \lambda_{1,2}=a\pm b\mathrm i λ1,2=a±bi,其中 a ≈ − 0.23 , b ≈ 0.79 a\approx-0.23,b\approx 0.79 a0.23,b0.79.所以递推方程 ( 4 ) (4) (4)的解有以下形式:
T ( m ) = C 0 λ 0 m + e a m ( C 1 sin ⁡ b x + C 2 cos ⁡ b x ) (7) T(m)=C_0\lambda_0^m+\mathrm e^{am}(C_1\sin bx+C_2\cos bx)\tag{7} T(m)=C0λ0m+eam(C1sinbx+C2cosbx)(7)

  后一项 I ( m ) = e a m ( C 1 sin ⁡ b x + C 2 cos ⁡ b x ) I(m)=\mathrm e^{am}(C_1\sin bx+C_2\cos bx) I(m)=eam(C1sinbx+C2cosbx)中,由于 a < 0 , m > 0 a<0,m>0 a<0,m>0,所以 ∣ I ( m ) ∣ < C 1 2 + C 2 2 |I(m)|<\sqrt{C_1^2+C_2^2} I(m)<C12+C22 是有界项,因此:
s t a t e _ c n t = T ( m ) = O ( λ 0 m ) = O ( 1.46 6 m ) (8) \mathrm{state\_cnt}=T(m)=O(\lambda_0^m)\tag{8}=O(1.466^m) state_cnt=T(m)=O(λ0m)=O(1.466m)(8)

  所以,结合 ( 3 ) (3) (3)式与 ( 8 ) (8) (8)式,可得算法的总时间复杂度为:
O ( n ⋅ λ 0 3 m ) = O ( n ⋅ 4. 4 m ) (9) O(n\cdot\lambda_0^{3m})=O(n\cdot 4.4^m)\tag{9} O(nλ03m)=O(n4.4m)(9)

  从式 ( 9 ) (9) (9)可以看出,复杂度与 n n n之间是线性关系,与 m m m之间是指数关系。所以题目给我们设置的 m m m范围 m ≤ 10 m\leq 10 m10也是相当合适的。但凡 m m m也和 n n n一样上限是 100 100 100,程序就炸了。
4. 4 10 ≈ 2.7 × 1 0 6 4. 4 100 ≈ 2.2 × 1 0 64 (10) \begin{aligned}4.4^{10}&\approx 2.7\times 10^6\\ 4.4^{100}&\approx 2.2\times 10^{64}\end{aligned}\tag{10} 4.4104.41002.7×1062.2×1064(10)

  所以,算法的复杂度 O ( n ⋅ 4. 4 m ) O(n\cdot 4.4^m) O(n4.4m)虽然是 m m m的指数函数,但是在 m m m比较小的情况下还是具有比较好的性能的,恰好适合题目给出的 m m m的数据范围。

  写到这里,又想到一些东西。比如,如果题目给出的 N × M N\times M N×M表格满足的条件是 N ≤ 10 , M ≤ 100 N\leq 10,M\leq 100 N10,M100的话,我们就需要按列DP;或者把表格转置过来以后,再按照上面的方法进行动态规划。其原因还是算法不允许 M M M达到过大的值。

最后

  上面的算法在POJ中跑出的结果是: M e m o r y   2964   K , T i m e   391   M S \mathrm{Memory\ }2964\ \mathrm K,\mathrm{Time\ }391\mathrm{\ MS} Memory 2964 K,Time 391 MS.第一次写题解,有不好的地方还请指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值