动态规划:状态压缩DP

蒙德里安的梦想:

状压DP之蒙德里安的梦想:
求方案数,首先因为他的摆法无非两种,而且要摆满,那其实就和染色问题(黑白两色)一样,对于某一种方案,一旦确定了横着放的情况,那么剩下的竖着放的方案也就唯一确定了。值得注意的是我们在枚举横着放的情况的时候,要想办法保证剩下的空间用来竖着摆放的合法性。

我们枚举横着摆放的情况时,对于第i列使用二进制数(状态j)来表示每个位置会不会放入一个横着摆放的棋子,为了避免混乱,我们每一列每一行横着摆放时,都选择将棋子最左端于本列最左端对其,那么每个棋子横置之后,必然会向右边相邻的列捅出去一格。
于是我们有两个边界条件:
1.f[[0][0]=1;第0列一个横置的都不放,因为我们只需要放满1~m列,显然第0列只有这一种摆法合法;
2.f[m][0];为最终的答案,因为枚举到最后一列,如果再放一个横置的,就会捅出右边界,所致最后一列一个横置的都不能再放了。这个跟之前算法竞赛进阶指南中费解的开关很像,当前面m-1列情况处理完之后,其实第m列就已经唯一确定了。

状态转移:f[i,j]+=f[i-1,k]其实很好理解,因为f[i,j]表示第i列以状态j放置时的合法方案数,那么他其实就等于前面所有的不和状态j冲突的摆法的总和。
关键是要保证不冲突,两个点:
1.不重。对于第i-1列,假设第p行已经放过了横置的,那么k的第p位就是1;现在第i列也要在第p行横置,这时j的第p位也是1.于是,显然重复覆盖了一个位置,不合法。于是我们要就当前状态j和前一列的状态k要满足j&&k==0才是合法的,才能进行状态转移;
2.不漏,当我们在满足1.的列放入所有的横置棋子之后,对于当前列即第i列的任何一个1X1的小格子,必然处于三种状态之一:被前一列横置棋覆盖、被当前列横置棋覆盖、空余。那么空余的当然不会被后面的列覆盖,而需要被竖着的棋子覆盖,于是每个空余的位置都必须连续成对存在,也就是i || k之中不能存在连续的奇数个0,而i || k范围有限,我们可以提前预处理出所有的i || k的状态是否合法st[i || k].
于是,每次枚举所有的当前列j行,和前一列k行,满足以上两点即可进行转移f[i][j]+=f[i-1][k].

#include 
#include 
using namespace std;

const int N = 12, M = 1<< N;  

long long f[N][M] ;// 第一维表示列, 第二维表示行,存的是方案数

bool st[M];  //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。

int m, n;

int main()
{
    //第一部分:预处理1。把不合法的方式去掉
    while (cin >> n >> m, n || m) //读入n和m,并且不是两个0即合法输入就继续读入
    { 
        memset(f,0,sizeof f);//初始化
        
        for(int i = 0; i < (1 << n); i ++) //先预处理每列不能有奇数个连续的0
        {
            st[i]=true;//假设成立,该状态没有奇数个连续的0则标记为true
            
            int cnt = 0 ;//记录当前这段连续的0的个数

            for(int j = 0; j < n; j ++) //遍历这一列,从上到下
            { 
                 if ( (i >> j) & 1) //如果该列的这行是1
                 {
                     //i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位; 
                     //&1为判断该位是否为1,如果为1进入if
                    if (cnt & 1)//看该列上面行连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
                        st[i]=false; 
                    cnt = 0; // 既然该位是1,计数器清零。
                 }
                 else cnt ++; //否则的话该位还是0,则统计连续0的计数器++。
            }
            if (cnt & 1)  //该列最下面的那一行判断一下连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
            st[i]=false;  
        }
        
        f[0][0]=1;//第0列不可能由前面捅过来,并且只需要放满1~m列,所以只有一种情况:竖着放
        
        //第二部分:预处理2
        // 经过上面每种状态 连续0的判断,已经筛掉一些状态。
        
        //下面来看进一步的判断:看第i-2列伸过来的、第i-1列伸出去 这两种情况:
        //因为i-2列伸过来会到i-1列上,所以i-1列就不能伸出去
        for(int i=1;i<=m;i++)
        {
            for (int j = 0; j < (1 << n); j ++) //枚举第i列的所有行状态
            {  
                for (int k = 0; k < (1 << n); k ++)//枚举第i-1列所有行状态
                { 
                    //再判断第i列中的1位置是否与i-1列冲突
                    if ((j & k ) == 0 && st[ j | k]) 
                    //f[i][j]:第i-1列中1的总个数
                    //来源两部分:
                    //f[i-1][k]表明第i-2列插入到i-1列的1的个数,i-1列被动生成的1
                    //f[i][j]表明第i-1列插入到i列的1的个数,也就是i-1列主动生成的1
                    f[i][j]+=f[i-1][k];
                }
            }
        }
        //上面的for循环i从1开始到m结束,但用到了i-1,所以其实f[i][]是从0开始的,最后一列为m-1,而m列就是判断是否有m-1列伸出来
        //f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
        //即整个棋盘处理完的方案数
        printf("%lld\n",f[m][0]);
    }

    return 0;
}   

最短Hamilton路径:

假设:一共有七个点,用0,1,2,3,4,5,6来表示,那么先假设终点就是5,在这里我们再假设还没有走到5这个点,且走到的终点是4,那么有以下六种情况:
first: 0–>1–>2–>3–>4 距离:21
second: 0–>1–>3–>2–>4 距离:23
third: 0–>2–>1–>3–>4 距离:17
fourth: 0–>2–>3–>1–>4 距离:20
fifth: 0–>3–>1–>2–>4 距离:15
sixth: 0–>3–>2–>1–>4 距离:18

如果此时你是一个商人你会走怎样的路径?显而易见,会走第五种情况对吧?因为每段路程的终点都是4,且每种方案的可供选择的点是0~4,而商人寻求的是走到5这个点的最短距离,而4到5的走法只有一种,所以我们选择第五种方案,可寻找到走到5这个点儿之前,且终点是4的方案的最短距离,此时0~5的最短距离为(15+4走到5的距离).(假设4–>5=8)

同理:假设还没有走到5这个点儿,且走到的终点是3,那么有一下六种情况:
first: 0–>1–>2–>4–>3 距离:27
second: 0–>1–>4–>2–>3 距离:22
third: 0–>2–>1–>4–>3 距离:19
fourth: 0–>2–>4–>1–>3 距离:24
fifth: 0–>4–>1–>2–>3 距离:26
sixth: 0–>4–>2–>1–>3 距离:17

此时我们可以果断的做出决定:走第六种方案!!!,而此时0~5的最短距离为(17+3走到5的距离)(假设3–>5=5)

在以上两大类情况之后我们可以得出当走到5时:
1.以4为终点的情况的最短距离是:15+8=23;
2.以3为终点的情况的最短距离是:17+5=22;
经过深思熟虑之后,商人决定走以3为终点的最短距离,此时更新最短距离为:22。

当然以此类推还会有以1为终点和以2为终点的情况,此时我们可以进行以上操作不断更新到5这个点的最短距离,最终可以得到走到5这个点儿的最短距离,然后再返回最初的假设,再依次假设1,2,3,4是终点,最后再不断更新,最终可以得出我们想要的答案

#include
#include
#include

using namespace std;

const int N=20,M=1<>j)&1)//i的路径中0~j,看这位是不是1,那么肯定i的j位必须是1才对
                for(int k=0;k>k)&1)//i的路径中表示0~k的路径,看这位是不是1,那么i的k位必须是1才对
                    //i-(1<

 

你可能感兴趣的:(算法基础,动态规划,算法)