插头DP_最小表示法 模板详解

声明

模板来自:https://www.cnblogs.com/kuangbin/archive/2012/09/29/2708989.html

算法说明

如果还不知道插头dp中插头以及轮廓线等概念是什么东西的话,请移步:插头与轮廓线与基于连通性状态压缩的动态规划

然后对于什么是最小表示法,请移步:《基于连通性状态压缩的动态规划问题  陈丹琪》

那么这里就简单说一下状态的转移:

对于m列的格子,用一个m+1的数组code来表示轮廓线上的信息(包括连通性)。
对于当前的格子(i,j),code[j-1]中是它左侧格子的插头信息,code[j]中时它上方格子的插头信息。
我们根据这两个值处理(i,j)的所有可能的状态,然后将 code[j-1]设为(i,j)下方格子的插头信息,code[j]设为(i,j)右侧插头信息
当j已经是最后一列时,我们将code数组的所有元素向右平移,将第一个元素code[0]设为0。

状态数量:
我们根绝左上角的插头状态来求解右下的插头状态!(这里的左上或者右下指的都是轮廓线转角那个地方上面或者下面)
首先是左上角的插头状态:
1)向右插头、向下插头
2)只有向右插头
3)最优向下插头
4)没有插头

他们分别对应的右下插头状态:
1)必须包含上插头和左插头
2)①向右的插头 ② 向下的插头
3)①向下的插头 ② 向右的插头
4)必须包含下插头和右插头

最后是最小表示法中联通分量的重新表示,对应如下:
1)因为没有下一个和右边的插头了 
① :当这个点是最终的点的时候,我们只需要将他们下侧和右侧的信息状态更新为0
② :不是最终的点时,我么需要将两个联通分量连接为同一个,将后面的合并为前一个联通分量中,然后下侧以及右侧信息状态更新为0
2 3)这两个状态的更新有些许的类似:
因为他们都会向右侧或者下侧更新,那么我们就只需要将右侧或者下侧的连通分量信息更新为上一个状态的就好,另一个更新为0(没有插头,也就没有连通性)

4)我们要重新开一个连通分量来进行计算,这里就用一个其他连通分量到达不了的数进行操作,在Encode函数中,他会被ch数组重新编码的

程序解释(URAL - 1519

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<algorithm>
#define LL long long
using namespace std;
/**
对于m列的格子,用一个m+1的数组code来表示轮廓线上的信息(包括连通性)。
对于当前的格子(i,j),code[j-1]中是它左侧格子的插头信息,code[j]中时它上方格子的插头信息。
我们根据这两个值处理(i,j)的所有可能的状态,然后将 code[j-1]设为(i,j)下方格子的插头信息,code[j]设为(i,j)右侧插头信息。
当j已经是最后一列时,我们将code数组的所有元素向右平移,将第一个元素code[0]设为0。
**/
const int MAXD=15;
const int HASH=30007;//一个比实际容量稍大的素数
const int STATE=1000010;//哈希表的最大元素个数
using namespace std;
int N,M;
int maze[MAXD][MAXD];
int code[MAXD];//表示这一行轮廓线上插头的信息
int ch[MAXD];//最小表示法使用
int ex,ey;//最后一个非障碍格子的坐标
struct HASHMAP
{
    int head[HASH],next[STATE],size;
    LL state[STATE];//和size相关联的状态
    LL f[STATE];//某种状态出现的次数
    void init()
    {
        size=0;
        memset(head,-1,sizeof(head));//用单独链表法处理碰撞
    }
    void push(LL st,LL ans)
    {
        int i;
        int h=st%HASH;
        for(i=head[h];i!=-1;i=next[i])
          if(state[i]==st)//找到了此键值
          {
              f[i]+=ans;//键值已存在,在这种状态下只是把次数加进去就好,否则添加状态
              return;
          }
        state[size]=st;//然后更新状态,将st状态加到相应的每一个数组
        f[size]=ans;
        next[size]=head[h];
        head[h]=size++;
    }
}hm[2];
void decode(int *code,int m,LL  st)//把某行上的轮廓信息解成一个code数组
{
    for(int i=m;i>=0;i--)
    {
        code[i]=st&7;//只去最后三位,也就是每一个占有三位,总共36位置,最大2^36
        st>>=3;
    }
}
LL encode(int *code,int m)//最小表示法 m<=12显然只有6个不同的连通分量
{    //将数组code状态压缩为st,转换为八进制的压缩,因为最多只有6个不同的状态
    //ch数组就是将code数组减小,变成有序的,这里就可以很好的将下面的13解释好
    int cnt=1;
    memset(ch,-1,sizeof(ch));
    ch[0]=0;
    LL st=0;
    for(int i=0;i<=m;i++)
    {
        if(ch[code[i]]==-1)ch[code[i]]=cnt++;//这里是给每一个状态编号,如果存在一个新的话,那么就新给一个号码,也就是连通的判断!
        code[i]=ch[code[i]];
        st<<=3;//0~7 很重要因为最多有6个状态,那么用二进制表示的话,也就是占用三个位置
        st|=code[i];
    }
    return st;//返回最终轮廓上的连通分量信息
}
void shift(int *code,int m)//当到最后一列的时候,相当于需要把code中所有元素向右移一位
{
    for(int i=m;i>0;i--)code[i]=code[i-1];
    code[0]=0;
}
void dpblank(int i,int j,int cur)//i,j表示当前位置、cur是当前状态,操作之后就是cur^1啦 总共就三大种情况
{
    int k,left,up;
    for(k=0;k<hm[cur].size;k++)
    {
        decode(code,M,hm[cur].state[k]);
        left=code[j-1];//左边的状态
        up=code[j];//上面的状态
        if(left&&up)//状态都存在
        {
            if(left==up)//在同一连通分量的情况下,只有最后一个节点才可以
            {
                if(i==ex&&j==ey)
                {
                    code[j-1]=code[j]=0;//最终合并成一个回路
                    if(j==M)shift(code,M);
                    hm[cur^1].push(encode(code,M),hm[cur].f[k]);
                }
            }
            else//不在同一个连通分量则合并成同一个
            {//只有一种情况,就是必选左上角的插头
                code[j-1]=code[j]=0;//那么下一个格子中的left以及up都会为0,因为在这里要连成同一个连通分量
                for(int t=0;t<=M;t++)
                  if(code[t]==up)   //将和上在同一个连通分量中的连接到向左的连用分量中
                    code[t]=left;
                if(j==M)shift(code,M);
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
        }
        else if((left&&(!up))||((!left)&&up))//只有一个有插头信息右下没有插头则连出来一个
        {//对于当前格子(i,j)code[j-1]是它左侧的格子插头信息,code[j]是它右边的格子插头信息
            //处理后:code[j-1]是(i,j)下方格子插头信息,code[j]是~右边格子插头信息
            int t;
            if(left)t=left;
            else t=up;
            if(maze[i][j+1])//右边没有障碍
            {//包含两种情况:有向右的插头 -> 指向右边 、 有向下的插头 -> 指向右边
                code[j-1]=0;
                code[j]=t;
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
            if(maze[i+1][j])//下边没有障碍
            {//有向右的插头 -> 组向下边 、 有向下的插头  -> 指向下边
                code[j-1]=t;
                code[j]=0;
                if(j==M)shift(code,M);
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
        }
        else//无插头 , 则构造新的连通块,因为所有都要被包含
        {
            if(maze[i][j+1]&&maze[i+1][j])
            {
                code[j-1]=code[j]=13;
                //那么这个点有必须有连通,那么我们就必须在这里添加一个右下的插头!
                //但是这里我们必须找一个别的连通分量到达不了的数组,方式和其他连通分量形成同一个连通分量,
                //那么我们的Encode函数不在乎这里的数字是多少,其中的ch数字会将code变为有序的数组!
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
        }
    }
}
void dpblock(int i,int j,int cur)//一个障碍是不可能有向下和向右的插头的,那就设其为0
{
    int k;
    for(k=0;k<hm[cur].size;k++)
    {
        decode(code,M,hm[cur].state[k]);//解码
        code[j-1]=code[j]=0;
        if(j==M)shift(code,M);//换行
        hm[cur^1].push(encode(code,M),hm[cur].f[k]);//毕竟是向后走了一格
        //把当前的数据cur=0压到另一个位置cur=1==>把当前的数据cur=1压到另一个位置cur=0
    }
}
char str[MAXD];
void init()
{
    memset(maze,0,sizeof(maze));
    //首先将所有的标记为有障碍,然后下面更新没有障碍的情况
    ex=0;
    for(int i=1;i<=N;i++)
    {
        scanf("%s",str);
        for(int j=0;j<M;j++)
        {
            if(str[j]=='.')
            {
                ex=i;
                ey=j+1;//记录最后一个位置
                maze[i][j+1]=1;//没有障碍的标记为1
            }
        }
    }
}
void solve()
{
    int i,j,cur=0;
    LL ans=0;
    hm[cur].init();//cur=0
    hm[cur].push(0,1);//加入没插头的状态cur=0
    for(i=1;i<=N;i++)
      for(j=1;j<=M;j++)
      {
          hm[cur^1].init();//每到一个位置,把另一组清零   清空cur=1==>清空cur=0
          if(maze[i][j])dpblank(i,j,cur);//当前这个进行设置
          else  dpblock(i,j,cur);
          cur^=1;
      }
    for(i=0;i<hm[cur].size;i++)//现在的cur要是放在循环里就是待计算的位置
      ans+=hm[cur].f[i];//各种状态的和就是总的可能的方案数
    printf("%I64d\n",ans);
}
int main()
{
    while(scanf("%d%d",&N,&M)!=EOF)
    {
        init();
        if(ex==0)//没有空的格子
        {
            printf("0\n");
            continue;
        }
        solve();
    }
    return 0;
}

后记

这个东西连连续续弄了三天才把这个模板看明白,当然这三天中两天在五一放假,昨天整整玩了一天,罪过啊、罪过,理解这个模板一定要把每种插头下状态的转移以及连通分量的转移画一遍,这样就很容易理解,希望可以帮助到你,上面的解释部分可能说的不是很明白,我没有再作图解释,希望大家谅解,如果有错误,欢迎大家在下方评论,谢谢!

猜你喜欢

转载自blog.csdn.net/li1615882553/article/details/80148656
今日推荐