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