【训练题15:环形DP(如何时间复杂度一降再降)】Naptime | SP283 | POJ 2228

Naptime | SP283
Naptime | POJ 2228

题外话

两个OJ的题目的时间复杂度都要求至少 O ( N 2 ) O(N^2) O(N2)
POJ额外要求空间复杂度 O ( N ) O(N) O(N)

看别人都是做成环形 D P DP DP (即做两次线性 D P DP DP
我这里用贪心思路把环形 D P DP DP 转换成一次线性 D P DP DP。(目前无发现反例)。

  • POJ的要求内存有点苛刻,需要使用滚动数组搞一搞。(补充代码二)

难度

提 高 + / 省 选 − \color{blue}提高+/省选- +/

  • 有趣的题目!
  • 主要是考察如何转移以及如何降低时间复杂度。
  • 后来还考察如何降低空间复杂度、、
  • 独立写出

题意

  • 在某个星球上,一天由 N N N 小时构成。
  • 在第 i i i 个小时睡觉能恢复 U i U_i Ui 点体力。
  • 在这座星球上住着一头牛,它每天要休息 B B B 个小时,它休息的这 B B B 个小时可以不连续,可以分成若干段,但是在每一段的第一个小时不能恢复体力,从第二个小时开始才可以恢复体力。
  • 为了身体健康,这头牛希望遵循生物钟,每天采用相同的睡觉计划。
  • 另外,因为时间是连续的,每天的第 N N N 个小时与下一天的第一个小时是相连的,这头牛只需要在 N N N 个小时内休息 B B B 个小时就够了。
  • 请你给这头牛安排一个任务计划,使得它每天恢复的体力最多。

数据范围

2 ≤ B < N ≤ 3830 2\le B < N \le 3830 2B<N3830
0 ≤ U i ≤ 200000 0 \le U_i \le 200000 0Ui200000

Sample

五天,睡三个小时
U = 2   0   3   1   4 U=2\ 0\ 3\ 1\ 4 U=2 0 3 1 4
则它睡 U 4 、 U 5 、 U 6 U_4、U_5、U_6 U4U5U6这三个时间段,获得收益为 1 ∗ 0 + 4 + 2 1*0+4+2 10+4+2

思路

1.【暴力枚举法(没学过DP)】

  • 暴力枚举每个时间段选或者不选,是最最简单的思路。
  • 时间复杂度 O ( 2 N N ) \color{cyan}O(2^NN) O(2NN)
  • 继续努力想想嘛。

2.【区间 D P DP DP / 三维 D P DP DP 写法】

  • d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示区间 i ∼ j i\sim j ij选择睡觉 k k k 个小时的获得最大体力值。
  • 考虑转移还要枚举中间断点,时间复杂度: O ( N 4 ) \color{cyan}O(N^4) O(N4)
  • 当状态维度过大,我们会试着降低一个维度看看效果。

3.【线性 D P DP DP / 二维 D P DP DP 单状态,多转移法】

  • d p [ i ] [ j ] dp[i][j] dp[i][j]表示区间 1 ∼ i 1\sim i 1i选择睡觉 j j j 个小时的获得最大体力值。
  • 转移方程我们可以轻松写出:
  • d p [ i + m ] [ j + m ] = max ⁡ { d p [ i ] [ j ] + g e t [ i + 1 ] [ j ] } dp[i+m][j+m]=\max\{dp[i][j]+get[i+1][j]\} dp[i+m][j+m]=max{ dp[i][j]+get[i+1][j]}
  • 其中 g e t [ a ] [ b ] get[a][b] get[a][b] 表示这一段区间的体力值的和,可以使用前缀和来优化到 O ( 1 ) O(1) O(1) 获得。
  • 我们可以发现,状态数有 O ( N 2 ) O(N^2) O(N2) , 每一个状态的转移种数又有 O ( N ) O(N) O(N)
  • 所以时间复杂度为 O ( N 3 ) \color{cyan}O(N^3) O(N3) (不加前缀和优化的话还要乘以一个 N N N
  • 尽管状态数少了,但是转移的方式还是很多,需要考虑如何转移才可以 O ( 1 ) O(1) O(1) 转移。

4.【线性 D P DP DP / 二维 D P DP DP 多状态,单转移法】

  • d p [ i ] [ j ] [ 0 ] dp[i][j][0] dp[i][j][0] 表示区间 1 ∼ i 1\sim i 1i选择睡觉 j j j 个小时,并且当前不睡觉时获得最大体力值
  • d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1] 表示区间 1 ∼ i 1\sim i 1i选择睡觉 j j j 个小时,并且刚刚睡觉一个小时时获得最大体力值
  • d p [ i ] [ j ] [ 2 ] dp[i][j][2] dp[i][j][2] 表示区间 1 ∼ i 1\sim i 1i选择睡觉 j j j 个小时,并且现在已经睡觉大于一个小时时获得最大体力值

这样,状态转移方程便可以很好地推出了

  • d p [ i ] [ j ] [ 0 ] = max ⁡ { d p [ i − 1 ] [ j ] [ 0 ] , d p [ i − 1 ] [ j ] [ 1 ] , d p [ i − 1 ] [ j ] [ 2 ] } dp[i][j][0]=\max\{dp[i-1][j][0],dp[i-1][j][1],dp[i-1][j][2]\} dp[i][j][0]=max{ dp[i1][j][0],dp[i1][j][1],dp[i1][j][2]}
  • d p [ i ] [ j ] [ 1 ] = d p [ i − 1 ] [ j − 1 ] [ 0 ] dp[i][j][1]=dp[i-1][j-1][0] dp[i][j][1]=dp[i1][j1][0]
  • d p [ i ] [ j ] [ 2 ] = max ⁡ { d p [ i − 1 ] [ j − 1 ] [ 1 ] , d p [ i − 1 ] [ j − 1 ] [ 2 ] } + U i dp[i][j][2]=\max\{dp[i-1][j-1][1],dp[i-1][j-1][2]\}+U_i dp[i][j][2]=max{ dp[i1][j1][1],dp[i1][j1][2]}+Ui
  • 式子一:当前不睡觉等于前一状态已经睡了 j j j 小时的最大值。
  • 式子二:当前睡第一轮等于前一状态少睡一个小时,并且还没睡觉的值。
  • 式子三:当前睡多轮等于前一状态少睡一小时,并且在睡觉的最大值。
  • 时间复杂度直接变成 O ( N 2 ) \color{cyan}O(N^2) O(N2),感觉已经成功了!

真的成功了吗?

  • 因为一天的时间不一定要从早上开始的一天,也可以是从中午开始到第二天中午的一天。比如样例,答案就是跨过一天的。
  • 如果我们枚举从哪一个时间开始作为一天的开始的话,时间复杂度又要再乘上一个 N N N ,似乎又成了一个瓶颈!

想法一:把一天扩展成两天

  • 如果一天是 U = 2   0   3   1   4 U=2\ 0\ 3\ 1\ 4 U=2 0 3 1 4,我们不试一下把两天拼接起来
  • 使之变成 U = 2   0   3   1   4   2   0   3   1   4 U=2\ 0\ 3\ 1\ 4\ 2\ 0\ 3\ 1\ 4 U=2 0 3 1 4 2 0 3 1 4
  • 然后再去计算最后的答案呢?

结果:

  • 这样会导致答案错误。比如他可能会同时在两天的上午睡觉,但这不符合题目要求。

想法二:越单纯越好

  • 考虑到:因为第一个小时的睡觉是不回复体力值的。如果要我们贪心的话,肯定是第一个小时会选择尽可能小的值。
  • 那我们为何不选出一个小时,这个小时是我们一定不会选择在这个小时获得休息值的呢? 然后选择把这个小时放在这天的第一个小时,当做新的一天的第一个小时。这样我们选择的结果就不会跨过新的一天的结尾了

这 样 子 就 成 功 了 ! \color{red}这样子就成功了!

核心代码 SP283 AC代码

时间复杂度 O ( N 2 ) O(N^2) O(N2)
空间复杂度 O ( N 2 ) O(N^2) O(N2)
T i m e s : 70 ( m s ) Times:70(ms) Times:70(ms)
M e m o r y : 146 ( M b ) Memory:146(Mb) Memory:146(Mb)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 4e3+50;
const int INF = 0x3f3f3f3f;

int aa[MAX];
int bb[MAX];
int dp[MAX][MAX][4];
int main()
{
    
    
    int T;scanf("%d",&T);
    while(T--){
    
    
        int N,B;
        scanf("%d%d",&N,&B);
        int mn = INF;
        int id = 0;
        for(int i = 1;i <= N;++i){
    
    
            scanf("%d",&bb[i]);
            if(bb[i] < mn){
    
    
                mn = bb[i];
                id = i;
            }
        }
        for(int i = 1;i <= N;++i){
    
    
            aa[i] = bb[(id + i - 1 - 1) % N + 1];
        }
        for(int i = 0;i <= N;++i)
        for(int j = 0;j <= B;++j)
        for(int k = 0;k <= 2;++k)
            dp[i][j][k] = -INF;

        for(int i = 0;i <= N;++i){
    
    
            dp[i][0][0] = 0;
        }
        for(int i = 1;i <= N;++i){
    
    
            for(int j = 1;j <= min(i,B);++j){
    
    
                dp[i][j][0] = max(max(dp[i-1][j][0],dp[i-1][j][1]),dp[i-1][j][2]);
                dp[i][j][1] = dp[i-1][j-1][0];
                dp[i][j][2] = max(dp[i-1][j-1][1],dp[i-1][j-1][2]) + aa[i];
            }
        }
        int ans = 0;
        for(int i = B;i <= N;++i){
    
    
            for(int j = 0;j < 3;++j){
    
    
                ans = max(ans,dp[i][B][j]);
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

Extra

但是POJ的内存限制太严格了咋办嘞?
我们使用滚动数组!
由于这里转移会用到 d p [ i ] dp[i] dp[i] d p [ i − 1 ] dp[i-1] dp[i1] ,我们不能每一行都滚动,那就隔一行滚一行:
使用一个奇偶数组,用二进制奇偶判别滚动即可。

核心代码二 POJ 2228 AC 代码

时间复杂度 O ( N 2 ) O(N^2) O(N2)
空间复杂度 O ( N ) O(N) O(N)
T i m e s : 94 ( m s ) Times:94(ms) Times:94(ms)
M e m o r y : 188 ( K b ) Memory:188(Kb) Memory:188(Kb)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 4e3+50;
const int INF = 0x3f3f3f3f;

int aa[MAX];
int bb[MAX];
int dp[2][MAX][3];
int main()
{
    
    
    int T;T = 1;
    while(T--){
    
    
        int N,B;
        scanf("%d%d",&N,&B);
        int mn = INF;
        int id = 0;
        for(int i = 1;i <= N;++i){
    
    
            scanf("%d",&bb[i]);
            if(bb[i] < mn){
    
    
                mn = bb[i];
                id = i;
            }
        }
        for(int i = 1;i <= N;++i){
    
    
            aa[i] = bb[(id + i - 1 - 1) % N + 1];
        }
        for(int i = 0;i <= 1;++i)
        for(int j = 0;j <= B;++j)
        for(int k = 0;k <= 2;++k)
            dp[i][j][k] = -INF;

        for(int i = 0;i <= 1;++i){
    
    
            dp[i][0][0] = 0;
        }
        int st = 0;
        for(int i = 1;i <= N;++i){
    
    
            for(int j = 1;j <= min(i,B);++j){
    
    
                dp[i&1][j][0] = max(max(dp[(i-1)&1][j][0],dp[(i-1)&1][j][1]),dp[(i-1)&1][j][2]);
                dp[i&1][j][1] = dp[(i-1)&1][j-1][0];
                dp[i&1][j][2] = max(dp[(i-1)&1][j-1][1],dp[(i-1)&1][j-1][2]) + aa[i];
            }
        }
        int ans = 0;
        for(int i = 0;i <= 1;++i){
    
    
            for(int j = 0;j < 3;++j){
    
    
                ans = max(ans,dp[i][B][j]);
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_45775438/article/details/109858577
今日推荐