动态规划:洛谷P1077 [NOIP2012 普及组] 摆花 一题多解 :DFS,记忆化搜索,二维DP,01背包问题,滚动数组优化,前缀和优化
P1077 [NOIP2012 普及组] 摆花
对于这题,有DFS,记忆化搜索,二维DP,01背包问题,滚动数组优化,前缀和优化多种解法,下面我来一一列出每种代码,叙述我的解题过程,这些题解方法也是我在学习了洛谷评论区各位大佬学习出来的经验。
一、DFS:
一开始我的思路是直接暴搜求解问题,我的代码为
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int mod = 1e6 + 7; 7 int a[105]; 8 int n, m; 9 int sum = 0; 10 void dfs(int num,int x) 11 { 12 if (num == 0) 13 { 14 sum++; 15 sum %= mod; 16 return; 17 } 18 for (int i = x; i <= n; ++i) 19 { 20 if (a[i] != 0) 21 { 22 a[i]--; 23 dfs(num - 1, i); 24 a[i]++; 25 } 26 27 } 28 } 29 int main() 30 { 31 cin >> n >> m; 32 for (int i = 1; i <= n; ++i) 33 cin >> a[i]; 34 dfs(m,1); 35 cout << sum; 36 return 0; 37 }
提交答案:
只过了四个点,还有六个点T了,我们看数据范围:
最多有100种,每种有100个,就是100*100=一万朵花里挑出100朵,可能太多了,显然暴搜会超时,于是就改进成为记忆化搜索。
二、记忆化搜索:
但是再原来的DFS上不太好改成记忆化,我们就稍微改一下,写出一个新的DFS,其中x代表的是现在搜索到的第几种,sum代表的是现在多少朵花了
代码:
1 #include<iostream> 2 using namespace std; 3 const int mod = 1e6 + 7; 4 const int N = 105; 5 int a[105]; 6 int n, m; 7 int dfs(int x, int sum) 8 { 9 if (sum > m) 10 return 0; 11 if (sum == m)//这个一定要放在 判断x是不是等于n+1的前面 因为x等于n+1的时候,sum要是刚好等于m 也是一种可能,因为此时说明n+1种花一个都不要 然后刚好凑到m朵花 12 return 1; 13 if (x == n + 1) 14 return 0; 15 int ans = 0; 16 for (int i = 0; i <= a[x]; ++i) 17 { 18 ans = (ans + dfs(x + 1, sum + i)) % mod; 19 } 20 return ans; 21 } 22 int main() 23 { 24 cin >> n >> m; 25 for (int i = 1; i <= n; ++i) 26 cin >> a[i]; 27 cout << dfs(1, 0); 28 return 0; 29 }
然后我们加入一个数组,DP[i][j],i代表当前枚举到第i种,j代表已经有多少花了,DP[i][j]代表从这个情况出发 可以有多少种可能到达M朵花。所以最后的ans就是DP[1][0],每次算出来就用DP数组存一下,如果搜索时候发现DP[][]!=0,直接返回里面的值。大大减少时间复杂度,也可以叫做剪枝,减去冗杂的枝条。
上代码:
1 #include<iostream> 2 using namespace std; 3 const int mod = 1e6 + 7; 4 const int N = 105; 5 int a[105]; 6 int b[105][105];//这个数组表示 从第x组,已经有y盆花开始,还能有几种方案到m盆 7 int n, m; 8 int dfs(int x, int sum) 9 { 10 if (sum > m) 11 return 0; 12 if (sum == m)//这个一定要放在判断x是不是等于n+1的前面 13 return 1; 14 if (x == n + 1)//如果x==n+1那么此时一定sum<m 不可能摆的成功 一定要注意这个顺序 15 return 0; 16 17 if (b[x][sum]) 18 return b[x][sum]; 19 int ans = 0; 20 for (int i = 0; i <= a[x]; ++i) 21 { 22 ans = (ans + dfs(x + 1, sum + i)) % mod; 23 } 24 b[x][sum] = ans; 25 return ans; 26 } 27 int main() 28 { 29 cin >> n >> m; 30 for (int i = 1; i <= n; ++i) 31 cin >> a[i]; 32 cout << dfs(1, 0); 33 return 0; 34 }
提交:
大大降低时间复杂度,成功通过!
三、DP
这题也非常的特殊,是我没怎么见到过的求和的DP,也就是他的状态转移方程是求和的形式,并不是很多的 max min形式。
建立状态转移方程:创建DP[i][j]数组,i代表循环到第i种,j代表现在的花朵数,dp[i][j]代表能凑成到第i种,一共有j朵花的种类数,所以我们得出状态转移方程:
int k=0,k<=a[i];k就是i种花能提供出来凑成j的花朵数。dp[i][j]=求和 【k从0到min(a[i],j)】
注意点:必须把DP[0][0]初始化为1,可以看做边界条件,否则,算DP[1][0] DP[1][1] ....算出来的答案都是0 具体可以举例试一下也就是说: DP[0][0]来初始化dp[1],dp[1]用于初始化dp[2]...
这里用到了三层循环 第一层枚举种类,第二层枚举总和,第三层就是求和计算的循环。
1 //P1077 [NOIP2012 普及组] 摆花 2 #include<iostream> 3 #include<cmath> 4 using namespace std; 5 const int maxn = 105, mod = 1000007; 6 int n, m, a[maxn], dp[maxn][maxn]; 7 int main() 8 { 9 cin >> n >> m; 10 dp[0][0] = 1;//重点 :一定要初始化为1 我们可以枚举第一种出来试一下,必须初始化DP[0][0]=1;下面的循环才能正常计算 11 //比如说第一种dp[1][1]=dp[0][0]+dp[0][1]=1;dp[1][2]=1等等 必须初始化DP[0][0]=1,才能把第一种的数组初始化 12 for (int i = 1; i <= n; ++i)cin >> a[i]; 13 for (int i = 1; i <= n; ++i) 14 for (int j = 0; j <= m; ++j) 15 for (int k = 0; k <= min(j, a[i]); ++k) 16 { 17 dp[i][j] = (dp[i - 1][j - k] + dp[i][j]) % mod;//注意取余 18 } 19 cout << dp[n][m]; 20 return 0; 21 }
四、滚动数组:
优化DP:观察三种,标红的这句话 DP[0][0]来初始化dp[1],dp[1]用于初始化dp[2]...,我们发现,每次也只用这个数组的当层和上一层,所以可以用滚动数组优化内存空间。
创建一个数组temp 用来复制,每次算出来的DP用memcpy存入temp,这样每次就只用数组的两层,也可以用一个只有两层的二维数组DP来,都可以。
上代码:
1 //P1077 [NOIP2012 普及组] 摆花 2 #include<iostream> 3 #include<cmath> 4 using namespace std; 5 const int maxn = 105, mod = 1000007; 6 int n, m, a[maxn], dp[maxn][maxn]; 7 int main() 8 { 9 cin >> n >> m; 10 dp[0][0] = 1;//重点 :一定要初始化为1 我们可以枚举第一种出来试一下,必须初始化DP[0][0]=1;下面的循环才能正常计算 11 //比如说第一种dp[1][1]=dp[0][0]+dp[0][1]=1;dp[1][2]=1等等 必须初始化DP[0][0]=1,才能把第一种的数组初始化 12 for (int i = 1; i <= n; ++i)cin >> a[i]; 13 for (int i = 1; i <= n; ++i) 14 for (int j = 0; j <= m; ++j) 15 for (int k = 0; k <= min(j, a[i]); ++k) 16 { 17 dp[i][j] = (dp[i - 1][j - k] + dp[i][j]) % mod;//注意取余 18 } 19 cout << dp[n][m]; 20 return 0; 21 }
注意每次都要把DP[j]定为0这样计算出的dp[j]才是正确的,否则会被之前算的影响,每次应该是互不影响。dp[j]的答案应该是前面的和,但dp[j]在上一层计算中是有数值的,所以应该定义为0,我在第一次提交时没注意这个问题,WA声一片...
提交数据:
五、01背包问题
观察发现,这题可以转化成一维的背包问题 每一个花朵数dp[j]都等于dp[j-k]的求和 所以可以三层循环
外层循环i代表种类数,对于每个第i种,都要内层循环一次花朵数j 花朵数要从大往小的循环,防止小的对大花朵数产生影响,做到无后效性。
常规的01背包dp[j]=max(dp[j-a[i]],dp[j]); 这题的dp[j]=sum k从1->a[i](dp[j-k]) sum可以写成第三层循环 用一个for循环来写。
所以代码就是:
1 //P1077 [NOIP2012 普及组] 摆花 2 #include<iostream> 3 #include<cmath> 4 using namespace std; 5 const int maxn = 105, mod = 1000007; 6 int n, m, a[maxn], dp[maxn]; 7 int main() 8 { 9 cin >> n >> m; 10 dp[0] = 1;//01背包问题 一定要初始化边界条件为1 11 for (int i = 1; i <= n; ++i)cin >> a[i]; 12 for (int i = 1; i <= n; ++i) 13 for (int j = m; j >= 0; --j)//01背包问题 从大的往小的算 14 for (int k = 0; k <= min(j, a[i]); ++k) 15 dp[j] = (dp[j] + dp[j - k]) % mod; 16 cout << dp[m]; 17 return 0; 18 }
但这样提交上去是错的,分析原因,一步一步调试发现,原来是最内层循环k要从1 开始 不能像前面二维循环一样从1开始写。
原因是如果这一种类,一个都不要,那dp[j]不需要加上dp[j-0]了,直接就是等于dp[j]自己,每层会多算一个dp[j]。而为什么二维dp就可以从0开始,原因可以分析状态转移方程,发现dp[][]是从0开始求和,需要加上一个都不要的情况。
正确代码:
1 //P1077 [NOIP2012 普及组] 摆花 2 #include<iostream> 3 #include<cmath> 4 using namespace std; 5 const int maxn = 105, mod = 1000007; 6 int n, m, a[maxn], dp[maxn]; 7 int main() 8 { 9 cin >> n >> m; 10 dp[0] = 1;//01背包问题 一定要初始化边界条件为1 11 for (int i = 1; i <= n; ++i)cin >> a[i]; 12 for (int i = 1; i <= n; ++i) 13 for (int j = m; j >= 0; --j)//01背包问题 从大的往小的算 14 for (int k = 1; k <= min(j, a[i]); ++k) 15 dp[j] = (dp[j] + dp[j - k])%mod; 16 cout << dp[m]; 17 return 0; 18 }
六、前缀和优化
01背包本题的时间复杂度是n的3方。我们可以优化时间复杂度,把发现最内层循环是求和,可以利用前缀和数组优化,注意特判数组是否越界。
上代码:
1 //P1077 [NOIP2012 普及组] 摆花 2 #include<iostream> 3 #include<cmath> 4 using namespace std; 5 const int maxn = 105, mod = 1000007; 6 int n, m, a[maxn], dp[maxn],sum[maxn];//前缀和数组优化 7 int main() 8 { 9 cin >> n >> m; 10 dp[0] = 1;//01背包问题 一定要初始化边界条件为1 11 //因为只有dp[0]=1,其他都是0 所以前缀和数组应该都是1 直接初始化前缀和数组为1 12 for (int i = 0; i <= m; ++i)sum[i] = 1; 13 for (int i = 1; i <= n; ++i)cin >> a[i]; 14 for (int i = 1; i <= n; ++i) 15 { 16 for (int j = m; j >= 1; --j)//01背包问题 从大的往小的算 这里j就到1 因为有j-1 而且dp[0]不需要计算了 17 { 18 //防止数组越界 特判一下 19 int x = j - min(j, a[i]) - 1; 20 if (x < 0)//如果数组越界 那越界的就不看 直接加上从0到j-1的前缀和 21 dp[j] = (dp[j] + sum[j - 1]) % mod; 22 else 23 dp[j] = (dp[j] + sum[j - 1] - sum[x]+mod) % mod; 24 //细节 必须加上一个mod 不然有可能是负数 25 } 26 //还要一个循环更新一下sum数组 27 for (int j = 1; j <= m; ++j)sum[j] = sum[j - 1] + dp[j]; 28 } 29 cout << dp[m]; 30 return 0; 31 }
完美结束。