题目描述
司令部的将军们打算在 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
N≤100,M≤10.
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}
f0f1⋯fM−1的值。对于地
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}
s0s1⋯sM−1为这一行炮兵放置的状态。显然,每个状态唯一地对应一个
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[i−1][k][l]+nrefl[j]}0if (j,k,l) is compatible,otherwise.(1)
其中,兼容(compatible)的条件是第 i , i − 1 , i − 2 i,i-1,i-2 i,i−1,i−2行的炮兵纵向不能相互攻击到,也就是没有哪两个炮兵在同一列上。如果兼容,那么在前 i i i行最大炮兵的数量,等于前 i − 1 i-1 i−1行最大炮兵数量加上第 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 i−1行的某一位(比如第 x x x位)都有炮兵,那么
srefl[j]
(对应第 i i i行的状态)和srefl[k]
(对应第 i − 1 i-1 i−1行的状态)的二进制表示中,第 x x x位都是 1 1 1,它们的按位与srefl[j] & srefl[k]
就非零。同样地,上面的代码块也可以用逻辑或(||
)代替按位或(|
)。
由于
i
=
0
i=0
i=0时没有第
i
−
1
,
i
−
2
i-1,i-2
i−1,i−2行,
i
=
1
i=1
i=1时没有第
i
−
2
i-2
i−2行,所以我们对于
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=0≤j<state_cnt0≤i<state_cntmax{refl[n−1][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(m⋅2m),这个很大程度上影响着整个算法的复杂度。
动态规划自底向上的过程用到了含state_cnt
的循环,复杂度为
O
(
n
⋅
s
t
a
t
e
_
c
n
t
3
)
O(n\cdot \mathrm{state\_cnt}^3)
O(n⋅state_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(m⋅2m+n⋅state_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
m−1块地有
T
(
m
−
1
)
T(m-1)
T(m−1)中放置炮兵的方式;如果第
0
0
0块地放置炮兵,那么第
1
,
2
1,2
1,2块地都不允许放置炮兵,所以剩下的
m
−
1
m-1
m−1块第有
T
(
n
−
3
)
T(n-3)
T(n−3)种放置炮兵的方式。可以得到以下递推方程:
T
(
m
)
=
T
(
m
−
1
)
+
T
(
m
−
3
)
(4)
T(m)=T(m-1)+T(m-3)\tag{4}
T(m)=T(m−1)+T(m−3)(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−λ2−1=0(6)
分析函数
f
(
λ
)
=
λ
3
−
λ
2
−
1
f(\lambda)=\lambda^3-\lambda^2-1
f(λ)=λ3−λ2−1可知,方程
(
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
a≈−0.23,b≈0.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(n⋅4.4m)(9)
从式
(
9
)
(9)
(9)可以看出,复杂度与
n
n
n之间是线性关系,与
m
m
m之间是指数关系。所以题目给我们设置的
m
m
m范围
m
≤
10
m\leq 10
m≤10也是相当合适的。但凡
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.4100≈2.7×106≈2.2×1064(10)
所以,算法的复杂度 O ( n ⋅ 4. 4 m ) O(n\cdot 4.4^m) O(n⋅4.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 N≤10,M≤100的话,我们就需要按列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.第一次写题解,有不好的地方还请指出。