# P1896 [SCOI2005]互不侵犯
作者:互联网
# P1896 [SCOI2005]互不侵犯
首先,看到这一题,就知道如果不是搜索,就是DP。当然搜索是过不了的,所以就应该尝试想出一个DP的解法。而且这还是一个状压dp(状压dp在此就不细讲了)
DP的前提之一当然是要找出一个可以互相递推的状态。显然,目前已使用的国王个数当然必须是状态中的一个部分,因为这是一个限制条件。那么除此之外另外的部分是什么呢?
我们考虑到每行每列之间都有互相的约束关系。因此,我们可以用行和列作为另一个状态的部分(矩阵状压DP常用行作为状态,一下的论述中也用行作为状态)。
又看到数据范围: 1 <=N <=9。这里我们就可以用一个新的方法表示行和列的状态:数字。考虑任何一个十进制数都可以转化成一个二进制数,而一行的状态就可以表示成这样——例如:
1010(2)
就表示:这一行的第一个格子没有国王,第二个格子放了国王,第三个格子没有放国王,第四个格子放了国王(注意,格子从左到右的顺序是与二进制从左到右的顺序相反的,因为真正在程序进行处理的时候就像是这样的)。而这个二进制下的数就可以转化成十进制:
10(10)
于是,我们的三个状态就有了:第几行(用i表示)、此行放什么状态(用j表示)、包括这一行已经使用了的国王数(用s表示)。
状态转移方程也就出来了:dp[i][j][k] += dp[i-1][l][s]就表示在只考虑前i行时,在前i行(包括第i行)有且仅有k个国王,且第i行国王的情况是编号为j的状态时情况的总数。而k就代表第i-1行的国王情况的状态编号)
再考虑国王之间的关系该如何处理呢?在同一行国王之间的关系我们可以直接在预处理状态时舍去那些不符合题意的状态,而相邻行之间的关系我们就可以用到一个高端的东西:位运算。
**这里介绍一些位运算相关的知识:**
1. ’&’符号,x&y,会将两个十进制数在二进制下进行与运算(都1为1,其余为0) 然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2. ’|’符号,x|y,会将两个十进制数在二进制下进行或运算(都0为0,其余为1) 然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3. ’^’符号,x^y,会将两个十进制数在二进制下进行异或运算(不同为1,其余 为0)然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4. ’~’符号,~x,按位取反。例如~101=010。
5. ’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。 ’>>’符号,是右移操作,x>>1相当于给x/2,去掉x二进制下的最右一位
1.判断一个数字x二进制下第i位是不是等于1。(最低第1位)
方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x=x|(1<<(i−1)) 证明方法与1类似。
3.将一个数字x二进制下第i位更改成0。
方法:x=x&~(1<<(i−1))
4.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x−1)
由于状态已经用数字表示了,因此我们可以用与(∧)运算来判断两个状态在同一个或者相邻位置是否都有国王——如果:
sit[j]&sit[l] (及上下有重复的king)
(sit[j]<<1)&sit[l] (及左上右下有重复king)
sit[j]&(sit[l]<<1) (及右上左下有重复king)
这样就可以处理掉那些不符合题意的状态了。
总结一下。其实状压DP不过就是将一个状态转化成一个数,然后用位运算进行状态的处理。理解了这一点,其实就跟普通的DP没有什么两样了。
最后上代码(注意其中的一些细节处理):
#include<iostream>
#include<cstdio>
using namespace std;
int n,k;
long long dp[10][15000][80]; //dp[i][j][k]表示第i行,状态为j,前面摆了k个国王时,方案数;
long long state[1000010] , king[1000010] ;//state[]是当前状态,king[]是当前行的国王数;
long long ans , sum;//ans是用来记录状态总数的,sum是用来计算一共有多少种方案的;
inline void iint()
{
int tot = (1<<n) - 1;//最多到这个时候,就是二进制下,每一位上都放上国王,当然有不行的,为了方便下文排除;
for(int i = 0 ; i <= tot ; i++)
if(!((i<<1)&i)) //因为要互不侵犯,所以,两个国王之间必须隔一个,这是判断是否满足题意国王之间不相互攻击;
{
state[++ans] = i; //找到了满足的,记录这个状态;
int t = i;
while(t) //判断这个状态有多少个国王,也就是t在二进制下有多少个1;
{
king[ans] += t%2;
t>>=1;
}
}
}
int main()
{
cin>>n>>k;
iint(); //初始化;
for(int i = 1; i <= ans ; i++) //先处理第一行;
if(king[i] <= k) //一行的国王数一定不能超过总数;
dp[1][i][king[i]] = 1;
for(int i = 2 ; i <= n ; i++) //处理剩下的,所以从 2 开始枚举;
for(int j = 1; j <= ans ; j++) //枚举状态;
for(int l = 1; l <= ans ; l++) //再一遍状态,用来当作上一行的状态
{ //处理相邻的行,
if(state[j] & state[l]) continue; //上下
if(state[j] & (state[l]<<1)) continue; //右上角
if((state[j]<<1) & state[l]) continue; //左上角
for(int s = 1 ; s <= k ; s++)
{ //s表示本行以上用了多少国王; //满足条件后,还要记得国王数量是有限的!!
if(king[j] + s > k) continue; //我们是递推,所以本行以上一定处理完了,所以,本行加以前用过的国王,总数不能超过限定;
dp[i][j][king[j]+s] += dp[i-1][l][s]; //dp[i][j][k]中的k表示已经用过的国王数,而king[]是本行的,s是本行以前的;
}
}
for(int i = 1; i <= n ; i++)
for(int j = 1 ; j <= ans ; j++)
sum += dp[i][j][k]; //本行及以前用光了国王,那么方案数加在总数中;
cout<<sum;
return 0;
}
完结撒花~~~~
标签:状态,互不侵犯,二进制,P1896,国王,SCOI2005,十进制,dp,10 来源: https://www.cnblogs.com/fzh050919/p/15239567.html