[BZOJ2669][cqoi2012]局部极小值(状压DP+容斥原理)

=== ===

这里放传送门

=== ===

题解

现在看这题的代码感觉这题写起来好GG啊。。是我当时太蠢了吗。。。没错是这样对吧。。。

首先局部最小值这个东西有一个很重要的性质就是局部最小值是不能相邻的,不然它们两个就会发生冲突然后就不知道到底谁是最小值了。所以这样的话在题目给定的这个格子大小范围内局部最小值的位置最多有8个。这样的话就可以把局部最小值状压起来啦。

考虑如果我们已经知道了哪些位置一定是局部最小值如何得到当前满足要求的方案数。肯定是用 1..nm 这些数字往格子里填。但是按照什么顺序填呢?如果一行行一列列那样填的话每个格子会被周围一圈格子影响所以这个递推顺序是不大对的。那么我们换一种方法,按照 1..nm 的顺序填,每次填到一个数字的时候,与它相关的局部最小值必须已经填上数字了。这样就能保证局部最小值一定比周围一圈的数字要小。

那么就可以用 f[k][S] 表示当前填到 k 这个数字,局部极小值的填写状态为S的方案数。对于 k 这个数字,如果它要填到第i个局部最小值里面,那么它的方案数就是f[k][S(1<<(i1))]这样。如果它不填到局部最小值里面呢?可以发现对于某些位置,如果会影响到它的局部最小值都已经填上数字了,那么这些位置在接下来的填写中就是等价的,就是先填谁都行。那么如果用 p[S] 表示局部极小值填写状态为 S 的时候等价位置的数目,在递推f[k][S]的时候就可以用 f[k1][S] 来更新答案。但是这个时候 f[k1][S] 乘上的系数并不是 p[S] ,因为在这 p[S] 个位置中可能有些已经填上数字了,也就是我们实际上要乘上的系数是 V=p[S](k1) 。这个东西也好算,如果设 S 这个状态中已经填写的局部极小值的数量为cnt,那么就有 V=p[S]+cnt(k1) 。p数组是可以用dfs来预处理的。

这样就可以了吗。。不不不这题才不会这么良心。。因为这么填写的时候只能保证题目给定的位置一定成为了局部极小值,但不能保证题目没有给定的位置不成为局部极小值,也就是可能有一些多出来的局部极小值。那就要用到容斥了。答案就是【初始答案】-【多出一个局部极小值的答案】+【多出两个局部极小值的答案】-【多出三个局部极小值的答案】+…

同样因为局部极小值的位置很少,所以可以用dfs爆搜当前的局部极小值状态。如果是题目已经给定的位置就直接记录,否则如果这个位置没有和任何已经存在的局部极小值相邻,它就是有可能成为极小值的位置。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define Mod 12345678
using namespace std;
int n,m,cnt,p[500],f[30][500],pos[10][2],ans,vis[10][10],ext[10][10];
int d[8][2]={{0,1},{0,-1},{1,0},{-1,0},{-1,1},{1,-1},{1,1},{-1,-1}};
void AddIn(int dlt,int x,int y){
    int tot=0;
    vis[x][y]+=dlt;
    for (int i=0;i<8;i++){
        int u=x+d[i][0],v=y+d[i][1];
        if (u<=n&&u>=1&&v<=m&&v>=1)
          vis[u][v]+=dlt;
    }
}
void work_p(int i,int now,int tot){
    int delta=0,x,y;
    if (i==cnt+1){
        for (int i=1;i<=cnt;i++)
          if ((now&(1<<(i-1)))!=0) ++delta;//把已经填过的极小值也算进去
        p[now]=tot+delta;return;
    }
    work_p(i+1,now,tot);
    x=pos[i][0];y=pos[i][1];
    for (int j=0;j<8;j++){
        int u=x+d[j][0],v=y+d[j][1];
        if (u<=n&&u>=1&&v<=m&&v>=1){
           ext[u][v]-=1;
           if (ext[u][v]==0) ++delta;
        }
    }
    work_p(i+1,now|(1<<(i-1)),tot+delta);
    for (int j=0;j<8;j++){
        int u=x+d[j][0],v=y+d[j][1];
        if (u<=n&&u>=1&&v<=m&&v>=1)
         ext[u][v]+=1;
    }
}
int Calc(){
    int tot=0;
    memset(f,0,sizeof(f));
    memset(p,0,sizeof(p));
    memcpy(ext,vis,sizeof(ext));
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++)
        if (ext[i][j]==0) ++tot;//计算所有极小值都没填的时候有多少位置能填数
    work_p(1,0,tot);
    f[0][0]=1;
    for (int i=1;i<=n*m;i++)
      for (int j=0;j<1<<(cnt);j++){
          f[i][j]=f[i-1][j]*(p[j]-i+1)%Mod;
          for (int k=1;k<=cnt;k++){
               int w=(1<<(k-1));
               if ((j&w)!=0) f[i][j]=(f[i][j]+f[i-1][j-w])%Mod;
          }
      }
    return f[n*m][(1<<cnt)-1]%Mod;
}
void Dfs(int x,int y,int tot){
    if (x>n||y>m) return;
    int nx,ny,cal;
    nx=x;ny=y+1;
    if (y==m){nx=x+1;ny=1;}
    Dfs(nx,ny,tot);
    if (vis[x][y]==0){
        pos[++cnt][0]=x;pos[cnt][1]=y;//增加一个多出来的极小值
        AddIn(1,x,y);
        cal=Calc();
        if ((tot+1)%2==0)
          ans=(ans+cal)%Mod;//容斥
        else ans=(ans-cal)%Mod;
        Dfs(nx,ny,tot+1);
        --cnt;AddIn(-1,x,y);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
      for (int j=1;j<=m;j++){
          char c=getchar();
          while (c!='.'&&c!='X') c=getchar();
          if (c=='X'){
              pos[++cnt][0]=i;pos[cnt][1]=j;
              AddIn(1,i,j);
          }
      }
    ans=Calc();
    Dfs(1,1,0);
    printf("%d\n",(ans%Mod+Mod)%Mod);
    return 0;
}

偏偏在最后出现的补充说明

n很小的时候基本上就是要么容斥要么状压
这个题恰好都用到了= =

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值