动态规划:洛谷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 }
DFS Code

提交答案:

 

 只过了四个点,还有六个点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 }
DFS Code 2

然后我们加入一个数组,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 }
Memory search

 

提交:

 

 大大降低时间复杂度,成功通过!

 

三、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 Code

 

 四、滚动数组:

优化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 Code

注意每次都要把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 }
01背包Code

但这样提交上去是错的,分析原因,一步一步调试发现,原来是最内层循环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背包Code true

 

 

六、前缀和优化

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 }
前缀和01背包优化Code

 

 

完美结束。

posted @ 2022-04-07 16:43  朱朱成  阅读(432)  评论(0编辑  收藏  举报