习题课3-2
刷油漆
-
n辆车排成1排,还有m种涂料,第i中涂料可以涂ai辆车,恰好可以涂n辆车,求有多少种涂油漆的方案,要保证相邻车辆油漆不同,
-
最后输出答案,除以23333的余数(答案非常大)
-
(a+b) mod c = [(a mod c) + (b mod c)] mod c
-
(aXb) mod c = [(a mod c) X (b mod c)] mod c
问题分析
- 排成一排->暗示我们顺序
- 恰好涂完->油漆不会剩余
- 相邻车的油漆种类不同
解法1(动态规划)(遍历涂完x辆车)
- 令f(x,p,c1,c2,…,cm)表示第x辆车到第n辆车,第x-1辆车涂的油漆种类是p,第i种油漆还剩下ci次,得到的方案数
- f(n+1,p,0,…0) = 1,(1<=p<=m)
- 到此截止,返回方案数1,动态规划必须考虑边界情况
- f(x,p,c1,c2,…,cm) = f(x+1,q,c1,c2,…,cq-1,…,cm)求和 (q不等于p)
- 其中n等于ai求和
- 最后的答案形式是f(1,0,a1,a2,…,am)
- 1是第一辆车开始,0是第0辆车涂的颜料种类,a1,a2,…,am是m种燃料还剩下的数量
- 可以循环计算也可以记忆化搜索
- 对x,p,c1,…,cm遍历循环,同时要保证没有后效性
- m不确定,不知道是几维数组,不好定义,状态数量有O(nm X (a1,…an累乘))
演示案例—记忆化搜索(假设是4种油漆)
-
// 假设是四维 // f(x,p,c1,c2,...,cm) int f[N][N][N][N][N][N],n; int dp(int x,int p,int c1,int c2,int c3,int c4){ if(x == n+1) return 1; // 把ans作为后边数组那一串的新名称 // 取地址符 int &ans = f[x][p][c1][c2][c3][c4]; // 记忆化搜索 // 已经计算过了,直接返回 if(ans !=-1) return ans; // ans=0后,f[x][p][c1][c2][c3][c4]也就等于0了 ans = 0; if(p!=1 && c1>0) ans += dp(x+1,1,c1-1,c2,c3,c4); if(p!=2 && c2>0) ans += dp(x+1,2,c1,c2-1,c3,c4); if(p!=3 && c3>0) ans += dp(x+1,3,c1,c2,c3-1,c4); if(p!=4 && c4>0) ans += dp(x+1,4,c1,c2,c3,c4-1); return ans; }
-
两层,外侧是dp,内侧是f(x,p,c1,c2,c3,c4)
-
// n辆车,m种油漆,第i种油漆够涂ai辆车,同时所有油漆恰好能涂完n辆车,若任意两辆相邻的车颜色不能相同,有多少种涂油漆的方案 // 返回值:方案数 int getAnswer(int m,vector<int> a){ // 一开始f初始化为-1,表示没有计算过 memset(f,-1,sizeof f); n = 0; for(int i=0;i<m;i++) n += a[i]; return dp(1,0,a[0],a[1],a[2],a[3]); }
解法1+
-
x的状态是多余的,x是可以通过ci来算出来的,即
-
x = 1 + ∑ i = 1 m ( a i − c i ) x = 1 + \sum_{i=1}^m(a_i-c_i) x=1+i=1∑m(ai−ci)
-
同时c_i全是0的时候涂油漆这个过程就结束了,因为
-
n = ∑ i = 1 n a i n = \sum_{i=1}^na_i n=i=1∑nai
-
而不需要判断x == n+1
-
即令
-
f ( p , c 1 , c 2 , . . . , c m ) f(p,c1,c2,...,cm) f(p,c1,c2,...,cm)
-
表示
-
x = 1 + ∑ i = 1 m ( a i − c i ) x = 1 + \sum_{i=1}^m(a_i-c_i) x=1+i=1∑m(ai−ci)
-
辆车到第
-
n = ∑ i = 1 m a i n = \sum_{i=1}^ma_i n=i=1∑mai
-
辆车,第x-1辆车涂的油漆种类是p,第i中油漆还剩下c_i次,得到的方案数
-
f ( p , 0 , . . . , 0 ) = 1 , ( 1 ≤ p ≤ m ) f(p,0,...,0)=1,(1\leq p \leq m) f(p,0,...,0)=1,(1≤p≤m)
-
f ( p , c 1 , c 2 , . . . , c m ) = ∑ q / p f ( q , c 1 , c 2 , . . . , c q − 1 , . . . , c m ) f(p,c_1,c_2,...,c_m) = \sum_{q/p}f(q,c_1,c_2,...,c_q-1,...,c_m) f(p,c1,c2,...,cm)=q/p∑f(q,c1,c2,...,cq−1,...,cm)
-
最后答案是
-
f ( 0 , a 1 , a 2 , . . . , a m ) f(0,a_1,a_2,...,a_m) f(0,a1,a2,...,am)
解法2(遍历用完c1,…,cm种油漆)
-
同样是动态规划
-
上一种解法状态太多,并且数组维度无法确定,尝试减少状态数
-
发现a_i<=5 (看题目的输出限制)
-
相同次数的油漆是不是可以作为一种
-
f(a,b,c,d,e,last)
- 够涂1辆车的种类有a中
- …
- 够涂5辆车的油漆还有e中
- 前一辆车的颜色是从上面5中取出来的,last==2表示前一辆车的油漆种类是从b中取出来的
-
f(0,0,0,0,last)==1表示一种方案
-
f ( 0 , 0 , 0 , 0 , l a s t ) = 1 , ( 1 ≤ l a s t ≤ 5 ) f(0,0,0,0,last) = 1,(1\leq last \leq 5) f(0,0,0,0,last)=1,(1≤last≤5)
-
f(a,b,c,d,e,last) = [a>0](a-[last==2])f(a-1,b,c,d,e,1)+ [b>0](b-[last==3])f(a+1,b-1,c,d,e,2)+ [c>0](c-[last==4])f(a,b+1,c-1,d,e,3)+ [d>0](d-[last==5])f(a,b,c+1,d-1,e,4)+ [e>0)ef(a,b,c,d+1,e-1,5)
-
其中[d>0]这种写法表示的是若d>0则为1,否则为0
-
时间复杂度O(m^max(a_i)*max(a_i)),用记忆化搜索更快
-
考虑第x辆车涂的颜色
-
若从第a类(第1类)取出一种颜色给x染色,a类里就会少1种颜色
-
last表示的是前一辆车涂的是哪一类(a,b,c,d,e或者叫1,2,3,4,5)油漆
-
之后是要涂第x+1辆车,因此是
-
x可以由a种选择上色(下式表示之前一辆车涂的油漆不来自于b类)
-
a * f(a-1,b,c,d,e,last=1)
-
或者是(下式表示之前一辆车涂的油漆来自于b类,那么a类里目前有一种和前一辆车相同的颜色的油漆)
-
(a-1) * f(a-1,b,c,d,e,last=1)
-
若从第b类(第2类)取出一种颜色给x染色,b类里就会少一种颜色,a类就会多一种颜色
-
(前一辆车的颜色不在c类里)
-
b * f(a+1,b-1,c,d,e,2)
-
(前一辆车涂的颜色在c类里)
-
(b-1) * f(a+1,b-1,c,d,e,2)
-
说明(c-[last==4)是要删除上一辆车是第4种燃料,避免相邻两辆车颜色相同
-
const int N = 21,mo = 23333; int f(N)(N)(N)(N)(N)(6); int dp(int a,int b,int c,int d,int e,int last){ // n==0,返回1,即空也表示1种方案 if(a|b|c|d|e==0){ return 1; } // 如果之前算过答案,直接返回 if(f(a)(b)(c)(d)(e)(last)!=-1){ return f(a)(b)(c)(d)(e)(last); } long long ret = 0; // 以下(last==2)等表达式的意思是:若这个表达式成立得到的是1,否则是0 if(a) // 若last==2,则表示上一辆车是从b里取出来的放到了a里,所以a中可以选择的油漆种类数要少1个 ret += dp(a-1,b,c,d,e,1) * (a - (last==2)); if(b) ret += dp(a+1,b-1,c,d,e,2) * (b - (last==3)); if(c) ret += dp(a,b+1,c-1,d,e,3) * (c - (last==4)); if(d) ret += dp(a,b,c+1,d-1,e,4) * (d - (last==5)); if(e) ret += dp(a,b,c,d+1,e-1,5) * e; return f(a)(b)(c)(d)(e)(last) = ret % mo; }
n皇后
- 一个nXn的棋盘,在棋盘上摆n个皇后,满足任意两个皇后不能在同一行、同一列或同一斜线上的方案有多少种
- 二进制优化?
- 考察搜索算法的剪枝水平,避免不必要的分叉口
解法1
-
搜索
-
枚举n个位置,判断是否满足条件,任两个皇后都不满足同行、同列、同斜线
解法2
-
搜索+回溯
-
枚举n个位置,判断是否满足条件,任两个皇后都不满足同行、同列、同斜线
-
搜索每一个位置的时候都判断一下是否满足条件,不满足就不往下搜索了
解法3
-
搜索+回溯+二进制优化
-
我们用一个整数、布尔来表示0和1是不是太浪费
-
一个二进制数字01010101,能表达的信息很多
-
搜索过程中我们三个二进制数字记录三个信息
- 列的分布情况a
- 右上到左下方向的对角线情况b
- 左上到右下方向的对角线情况c
-
若二进制位置上是1,则表示该位置已经放过皇后
-
在一行中,能放皇后的位置是(a|b|c)为0的位置
-
对一个二进制找0,等价于取反后找1
-
如何快速找到二进制里的一个1?
-
lowbit(x) = x & -x
-
原数与补码做与运算
-
-x是补码(取反再加1)
-
比如
-
lowbit(111010) = 000010, lowbit(11100) = 00100,
-
也就是说lowbit(x)得到的是二进制位最低的1
-
计算机补码表示的,所以-x表示先将每一位取反,然后加1得到的数,然后再与x取个并,自然就得到了最低位的1
-
0 1 0 0 0,放了以后a=01000,b=10000,c=00100 0 0 0 1 0,放了以后a=01010,b=00100,c=00011 1 0 0 0 0,放了以后a=11010,b=01000,c=01001 0 0 1 0 0,放了以后a=11110,b=10000,c=00110 0 0 0 0 1,最后只能放这一个位置
-
// allOne是二进制全为1的排列 int ans,allOne; // 搜索,用二进制优化 // pos:其二进制上的某个位置的1表示当前所在行的的相应的位置(列)已经放过一个皇后 // left:其二进制上的某个位置的1表示当前所在行的相应的位置(是由于右对角线上已有皇后)不能放置皇后 // right:其二进制上的某个位置的1表示当前所在行的相应的位置(是由于左对角线上已经有皇后)不能放置皇后 void dfs(int pos,int left,int right){ // 当且仅当每一列都放了一个皇后那么整个棋盘已经放了n个皇后,故要终止 if(pos == allOne) { ++ans; return; } // t还有1存在,找出所有t中的1 // t代表可以放的集合对应的二进制数 // allOne在这里是起截断作用的,放置越界 for(int t=allOne & (~(pos|left|right));t>0;){ // low bit,二进制中最右边的1 int p = t & -t; dfs(pos|p,(left|p)<<1,(right|p)>>1); // 消掉low bit t ^= p; } } int getAnswer(int n){ ans = 0; allOne = (1<<n)-1; dfs(0,0,0); return ans; }
解法4
- 提交答案
- OEIS,http://oeis.org