动态规划:洛谷 P4933 大师 (一题多解)

洛谷 P4933 大师

 

 

 

     题目和数据范围如上。这是洛谷一题普及+/提高的题目,考点是动态规划。有以下几种做法:

一、O(n2 v)

    利用三层循环DP,复杂度高,暴力。最外层循环枚举公差,公差的选择:最高的和最矮的差值是公差的最大值,他的负数是公差的最小值。内层有两个循环,枚举两个数是否能构成等差,如果可以,状态转移方程为

    dp[i][d]=sum( dp[j][d]+1);(j是所有在i前面的元素),i/j代表的是以i/j结尾的序列,d代表的是公差+20000,因为存在公差为负数的情况,所以我们+20000,全部右移一个区间,缺点是浪费空间。为什么是这个状态转移方程呢,如果i j能构成等差 种类数相当于 j的所有序列的尾巴都加上一个i 数量是dp[j][d],+1就是 和h[i] 和h[ j]两个独自构成一个等差。

    上代码:

 1 //洛谷 P4933 大师
 2 #include<iostream>
 3 #include<cmath>
 4 using namespace std;
 5 int h[1005], dp[1005][40005];//代表的是对于以i结尾的,公差为j+20000最多的可能性
 6 const int mo = 998244353;
 7 int ans;
 8 int main()
 9 {
10     int n;
11     cin >> n;
12     int m = -1;//记录max
13     for (int i = 1; i <= n; ++i)
14         cin >> h[i], m = max(m, h[i]);
15     ans += n;
16     for (int i = -m; i <= m; ++i)
17     {
18         for (int j = 1; j <= n; ++j)
19         {
20 
21             for (int k = 1; k < j; ++k)
22             {
23                 if ((h[j] - h[k]) == i)
24                 {
25                     dp[j][i + 20000] += dp[k][i + 20000] + 1;
26 
27                 }
28             }
29             ans += dp[j][i + 20000];
30             ans %= mo;
31         }
32 
33     }
34     cout << ans;
35     return 0;
36 }
O(n2v) code 1

·

 

 只过了12个测试点,还有8个超时,这样很显然的,毕竟时间复杂度太大了,想着改进算法。

 

二、O(n3)

    DP数组修改一下,dp[i][j],i代表以i结尾的序列,j表示倒数第二个元素是j的序列,这样有两个优点,一:DP的的空间复杂度小了,第二维不用开公差了,公差远大于种类数,空间复杂度高,我们只要用h[i]-h[j]就能表示公差,二:最外层循环不需要枚举公差了,最外面两层循环枚举每一个i 和 j 里面一层循环枚举k,我们让 i>j>k,利用数学结论,如果k j i 构成等差数列,那么h[j]*2=h[i]+h[k];所以h[j]*2=h[i]+h[k]成立时当所以i和j的DP一定是由j 和 k传递来的,并且dp[i][j]+=dp[j][k],因为i j只是相当于在 j k的所有序列的情况的末尾都加上一个 i 。三层循环的时间复杂度为n3,比n2v降低了。

    上代码:

 1 //洛谷 P4933 大师
 2 #include<iostream>
 3 #include<cmath>
 4 using namespace std;
 5 int h[1005], dp[1005][1005];//代表的是对于以j结尾的,上一个是i的种类数
 6 const int mo = 998244353;
 7 int ans;
 8 int main()
 9 {
10     int n;
11     cin >> n;
12     for (int i = 1; i <= n; ++i)
13     {
14         cin >> h[i];
15         dp[0][i] = 1;//表示只有一个时候 有一种可能性
16     }
17     ans += n;//先把都只有一个的时候的可能性加上
18     for (int i = 2; i <= n; ++i)//用两层循环来表示公差
19         for (int j = 1; j < i; ++j)
20         {
21             dp[j][i] = 1;//初始化一下,h[i],h[j]起码能构成一个等差数列 
22             for (int k = 1; k < j; ++k)
23             {
24                 if (h[j] * 2 == h[i] + h[k])//如果k j i 构成等差数列
25                 {
26                     dp[j][i] += dp[k][j];
27                     //相当于把以k为倒数第二个,j为倒数第一个的所有序列尾巴上都加上一个i 但是种类数还是不变得
28                     dp[j][i] %= mo;
29 
30                 }
31             }
32             ans += dp[j][i]; //以二维循环的形式累加到ans,这样保证了每一种dp只累加一次到ans 无后效性,所有可能:以i为结尾,j为倒数第二个的每一种可能都遍历到,并且只加一次到ans。
33             ans %= mo;
34         }
35     cout << ans;
36 
37     return 0;
38 }
O(n3)code 2

 

 

三、O(n2)

    根据2的思路,我们还可以继续优化,我们两重循环枚举 i j 那么 i和 j一定会构成一个公差d.所以我们不用表示公差 直接用 i j 的高度差表示公差,两层循环,dp[i][d]的含义改变,i代表结尾,d代表公差,则状态转移方程: dp[i][d]=sum (dp[j][d]+1) j代表所有小于i的数字 状态转移方程同1并且原理相同.

上代码:

 1 //洛谷 P4933 大师
 2 #include<iostream>
 3 #include<cmath>
 4 using namespace std;
 5 int h[1005], dp[1005][40000];//代表的是对于以i结尾的,公差为j-20000的种类数
 6 const int mod = 998244353;
 7 int ans;
 8 int main()
 9 {
10     int n;
11     cin >> n;
12     for (int i = 1; i <= n; ++i)
13     {
14         cin >> h[i];
15     }
16     int p = 20000;
17     for (int i = 1; i <= n; i++)
18     {
19         ans++;//只有一种的情况
20         for (int j = i - 1; j > 0; j--)
21         {
22             int d = h[i] - h[j] + p;
23             dp[i][d] += dp[j][d] + 1;//加上dp[j][d]是相当于让以h[j]结尾 公差一样的序列 每个尾巴都加上一个h[i]
24             //此时一定还构成等差序列 加上1是要加上h[j] h[i]这个序列 这两个也一定构成等差 
25 
26             ans += dp[i][d];
27 
28             dp[i][d] %= mod;
29             ans %= mod;
30         }
31     }
32     cout << ans;
33 
34     return 0;
35 }
O(n2)wrong code

但是提交发现:

 

 错了。调试检查发现原因,原因在新的代码中注释了,测试案例就是 出现一样的高度的铁塔情况,可以自己试试。

上代码:

 1 //洛谷 P4933 大师
 2 #include<iostream>
 3 #include<cmath>
 4 using namespace std;
 5 int h[1005], dp[1005][40000];//代表的是对于以i结尾的,公差为j-20000的种类数
 6 const int mod = 998244353;
 7 int ans;
 8 int main()
 9 {
10     int n;
11     cin >> n;
12     for (int i = 1; i <= n; ++i)
13     {
14         cin >> h[i];
15     }
16     int p = 20000;
17     for (int i = 1; i <= n; i++)
18     {
19         ans++;//只有一种的情况
20         for (int j = i - 1; j > 0; j--)
21         {
22             int d = h[i] - h[j] + p;
23             dp[i][d] += dp[j][d] + 1;//加上dp[j][d]是相当于让以h[j]结尾 公差一样的序列 每个尾巴都加上一个h[i]
24             //此时一定还构成等差序列 加上1是要加上h[j] h[i]这个序列 这两个也一定构成等差 
25 
26             ans += dp[j][d] + 1;//注意点!这个只能加上前一项的dp[j][d] + 1 ,不能加上dp[i][d],因为如果有两个j满足公差一样,那么dp[i][d]的
27             //初始值就并不是0 那么ans直接加dp[j][d] + 1,两个j公差相等的话,则第一次加后dp[i][d]已经不等0,第二次直接加dp[i][d]会把第一次的答案重复加一次 使答案偏大
28             //如果dp的第二个元素是j那么就没事,但这里是d 不同的j可能h[j]相同 所以不行 
29 
30             dp[i][d] %= mod;
31             ans %= mod;
32         }
33     }
34     cout << ans;
35 
36     return 0;
37 }
O(n2)true code

 

 非常完美,时间复杂度也杠杠的,就是空间复杂度变大了,牺牲空间换时间呀。

 

四、其实我看了题解,很多都提到这题本来的正解是用nv的时间复杂度,虽然大于n2,但是我也很想思考写出这个算法,奈何实在写不出来,题解中也没有找到有人写了这种算法,只能暂且搁下,灵感来的时候补全nv的代码。他的思路是dp[i]=sum[h[i]-d];sum[h[i]]+=dp[i];sum数组存所有高度为h的dp和,dp[i]数组就等于高度比他矮d的所有dp[]之和,用sum数组来存,代码暂时写不出来,就写一个思路。

posted @ 2022-04-12 00:07  朱朱成  阅读(213)  评论(1编辑  收藏  举报