• DP:Corn Fields(POJ 3254)


                       北大教你如何高效养牛(误)(点我查看)

        2015-08-21:

      问题的大意就是有一片稻田,里面有很多坑,你要在上面种稻谷,然后呢田里面还会养牛,牛不喜欢扎堆吃饭,所以呢你种的稻谷要间隔种在坑里面,所以一个种了稻谷的坑的上下左右4个坑都不能再种稻谷,而且呢,这些坑有些坑是贫瘠的,不能种稻谷(这不是坑爹吗,难道是有毒?),嗯,大概就是这样,现在那个问你给你一片田问你有多少个种植稻谷的方案

     

                          

      

      这个问题,看上去挺烦的,你又要想着如何不违反规定(种了田上下左右不能种田),还有些地不能种,还要算出最后的方案数,然后你想一下,如果你确定了一个可以种的方案,还不行,或许你和前面可以种的方案冲突,又或者他根本就不是最大的方案数………………………………完了,这题又没法做了

                          

      别急,我们来想办法,我们想着如何简化我们的思路,首先,我们要确立我们思考的方向,一下子考虑全局肯定是不行的了,我们可以把我们的目光放小一点,我们只考虑一行,或者一列这样子(一般做这样的题都必须这样想,包括LIS,LCS,01背包,完全背包统统都是这种思路)

      当然这题肯定是考虑一行比较方便了(你考虑一列也行,但是后面有个操作会很不方便,等一下说),然后对应两种大情况:

      ①左右坑冲突的解决:

      我们可以是根据不能种稻米的坑来确定是否能种植,然后列出所有方案,还可以是先确定所有可行的种植方案,然后再来判断是否和贫瘠坑发生冲突,这两种情况。

      ②上下行冲突的解决:

      上下行也是和左右的冲突解决是一样的,也是比较而已,也是可以像左右那样可以采取两种方案,当然了我们不必上下行都看,我们只用看上一行就可以了,因为我们是从上到下一行一行扫描的,也就是说,所有的本行只和上一行的状态有关,(上上行和上行的关系早就处理好了),如果你想到了这一点,那么这题就完成了一半了

      回过头来我们想一下应该采取哪种方案,很明显,如果我们是先根据坑的情况确定种植的话,那这题的复杂度就大大上升了,为什么?你想啊我们现在不仅是要和本行的左右结构进行冲突的解决,还要对上下结构的冲突进行解决,那么我们起码要保留两个方案的组合,而且每一次方案还要重新构建一遍可行方案,而且还有保存组合数目的问题,实在是太麻烦了,所以我们果断采取先确定所有可行的种植方案,然后再来判断是否和贫瘠坑发生冲突的方法

      做法其实挺简单的,首先我们先把所有可行的状况全部找出来,存在一个地方中,然后每行处理的时候我们一个个拿出来,再和贫瘠坑作对比看有没有冲突,然后然后再和上一行比较看是否冲突,然后我们再来看怎么解决组合的储存问题,因为我们已经储存了所有可行的方案,然后可行的方案又会和所有的可行方案进行比较(即和上一行进行比较),这样一来,我们本行的可行方案数是和可行状态有关,这么看,我们只用把可行方案数“存”在可行状态上就可以啦,然后每次只用与上一行每一个可行的状态累加,存在本行状态上就可以了!多方便。

      讲到这里,这题已经很明了,我们一下子断定,这一题一定是一道DP,而且是一个类型的DP,叫状态压缩听上去好像有点厉害的样子~

                            

      

     

      好了思路讲完了,不过先别着急着码代码,我们来解决几个小细节,就是这题比较特殊,因为能不能种只有两个情况,要么种,要么不种,总不能种半棵对吧,那么我们就可以采取0和1来标记坑了(包括贫瘠坑也可以这么标记,当然这题题目已经提示你了),如果是这样,那么我们就可以用二进位来表示这些东西了,这样的话,我们存的状态只用存一些二进位,那么就不用二维数组了,不过这样就要用到位运算,可能会给一开始接触这些题的人带来困惑。(我一开始就是这样,根本看不懂别人写的是什么,用位运算干嘛啊,然后看了半天才明白)。

      为了防止掉坑,我在我自己的代码对位运算加了挺多的注释,一开始看不懂位运算的朋友可以看注释理解理解~

      然后就是一个优化问题,为了方便差不多网上所有的ACMer的代码这题都是用的二维数组,当然你看了我上面的解释就知道这题可以只用两个数组,直接降了差不多一半的内存占用量(当然这题的规模比较小,而且用二维数组实际上是不会多多少内存的,毕竟65536K的内存限制,太宽松了),速度不会差多少

    代码如下:(建议复制到IDE去看)

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    long long Find(int *, int, int);
    void Inivilize_Valid_State(int *,const int,int *);
    int if_valid(const int, const int);
    void Inivilize_now_and_prev(int **, int **, int);
    
    int main()
    {
        int M, N, i, j, tmp;
    
        while (scanf("%d %d", &M, &N) != EOF)//输入行和宽
        {
            int *S = (int *)malloc(sizeof(int)*(M + 1));//因为这是状态01,可以用位运算来表示
    
            for (i = 1; i <= M; i++)//输入耕种的图
            {
                S[i] = 0;//首先定义全部为0再说
                for (j = 1; j <= N; j++)
                {
                    if (scanf("%d", &tmp) &&!tmp)
                        S[i] |= 1 << (N - j);
                        //现在是不合法的时候状态为1
                        //S[i] |= 1 << (N - j)的意思是直到出现一个1,那么我们就把当前的数字往前挪N-j位(把0移走)
                        //‘|=’就是‘+1’的意思(当然你也可以直接写+1),把1放到最低位
                }
            }
            printf("%I64d", Find(S, M, N));
            free(S);
        }
    }
    
    void Inivilize_Valid_State(int *State,const int n,int *top)
    {
        int i = 0;
        for (; i < n; i++)
            //这里的意思是把区域都划分出来,比如只有三列,那么只有101,010,100,001,这些位置是合法位置,其他都会不行
            //一定要注意,0也是一个合法的位置(什么都不种)
            if (!(i&(i << 1)))
                State[(*top)++] = i;
    }
    
    int if_valid(const int valid_state, const int x)
    {
        //用位运算的方法,如果合法,比较一些二进位,如果有位置同时为1,那么就会返回不合法,否则就是合法位置
        if (valid_state&x) return 0;
        else return 1;
    }
    
    void Inivilize_now_and_prev(int **now, int **prev, int valid_sum)
    {
        *now = (int *)malloc(sizeof(int)*valid_sum);
        *prev = (int *)malloc(sizeof(int)*valid_sum);//两个数组循环交替
        memset(*now, 0, sizeof(int)*valid_sum);
        memset(*prev, 0, sizeof(int)*valid_sum);
    }
    
    long long Find(int *S, int line, int col)
    {
        int i, j, sum = 0 , valid_sum = 0, past;
        int *now = NULL, *prev = NULL, *tmp = NULL, *valid_state = NULL;
        const int mod = 100000000;
    
        valid_state = (int *)malloc(sizeof(int)*(1 << col));
        Inivilize_Valid_State(valid_state, (1 << col), &valid_sum);
        Inivilize_now_and_prev(&now, &prev, valid_sum);
    
        for (j = 0; j < valid_sum; j++)//初始化一的情况
        {
            if (if_valid(valid_state[j], S[1]))
                prev[j] = 1;
        }
    
        for (i = 2; i <= line; i++)
        {
            for (j = 0; j < valid_sum; j++)//遍历合法位置
            {
                if (if_valid(valid_state[j], S[i]))//如果当前位置不与贫瘠坑发生冲突
                {
                    for (past = 0; past < valid_sum; past++)
                    {
                        if (if_valid(valid_state[past], S[i - 1]) && !(valid_state[j] & valid_state[past]))
                            //上一个情况没有一个在不可种的位置,且当前位置与下一个位置不冲突
                            now[j] = (prev[past] + now[j]) % mod;//记得取余
                    }
                }
            }
            tmp = now; now = prev; prev = tmp;
            memset(now, 0, sizeof(int)*valid_sum);
        }
        for (i = 0; i < valid_sum; i++)//把最后一行所有的组合数加起来
            sum = (sum+ prev[i])%mod;
    
        free(now); free(prev); free(valid_state);
        return sum;
    }

    PS:最后,说一下我自己的写代码的风格:

      我是很讨厌直接把变量写成a,b,c,d,s……这样的,而且全局变量一大堆,真的很讨厌,但是几乎所有的ACMer都是这样写代码的,思路都是对的,但是写出来就让人看半天。

      而你们看我的代码,我是非常讨厌写全局变量的,所以我宁愿多用几个函数,多传几个指针,我都不想一个全局变量摆在main上面(当然全局变量写上去的确省时间),但是这样我的代码量通常都会比别人多差不多一半。

      对于我自己来说,我来玩ACM是业余爱好(现在我的心态摆正了,ACM是真的是有天赋的人玩的,而我不是),纯粹就是为了巩固自己的算法基础,拿奖什么的就不奢求了,我坚持的是我自己的代码我自己要看的明白,别人也要看的明白(学过编程的),所以我一直奉行一个观念:会写好的变量名,好的函数名,是好程序的开始,比你写一百个注释强多了。以后出来工作都是集体项目,不可能你自己写的代码只有你自己看的懂,别人看要看半天才明白你写的什么,这样的程序员,是失败的。

  • 相关阅读:
    测试项目框架搭建
    项目实战(一)
    接口和HTTP协议(二)
    接口和http协议(一)
    什么是DFX测试
    完美解决安装在虚拟机中的CentOS7无法联网的问题
    RIP动态路由协议
    ensp实验--------RIP动态路由实验
    ensp实验--------telnet登录认证
    CSMA/CD协议
  • 原文地址:https://www.cnblogs.com/Philip-Tell-Truth/p/4748344.html
Copyright © 2020-2023  润新知