「AT1732」「CODE FESTIVAL 2015 OKINAWA OPEN」Jungle
       
      
    
   
  
  题目传送门 
题意 
有 \(n\)  棵树,第 \(i\)  棵树高度为 \(a_i\) 。你需要砍掉一些树,砍树规则如下:
只能砍最多 \(m\)  棵树。 
对于每个 \(i\ (1\le i\le n-k+1)\) ,满足 \([i,i+k-1]\)  中被砍掉的树的数量不超过 \(1\) 。 
 
被砍掉的树的位置的高度都变为 \(0\) 。
求砍树后 \(\max\limits_{1\le i\le n-k+1}\sum\limits_{j=i}^{i+k-1} a_j\)  的最小值,即最小化所有长度为 \(k\)  的区间的树的高度之和的最大值。
\(n\le 10^5,a_i\le 10^9\) 
 
 一个错误但可以 AC 的贪心解法 
求出 \(sum_i\)  表示以 \(i\)  结尾的长度为 \(k\)  的区间的 \(a_i\)  之和。然后求出 \(mx_i\)  表示所有包含 \(i\)  的长度为 \(k\)  的区间中,最大的 \(sum_j\)  减去 \(a_i\)  的值,即 \(mx_i=\max\limits_{i\le j< i+k}sum_j-a_i\) 。
显然可以二分。二分答案 \(x\)  后,问题变成了一个判定性问题。
是否存在一种砍树的方法,使得所有长度为 \(k\)  的区间的树的高度之和都不超过 \(x\) 。
 
可以发现,若一棵树 \(i\)  能砍,必须满足 \(mx_i\le x\) 。对于每个不符合要求的区间,选择该区间中最后一棵能砍的树砍掉。可以发现,优先选最后一棵有时候是不优的,例如
 
这种情况(O 表示可以砍的树,X 表示不能砍的树,[...] 表示不符合条件的区间),显然方案是选两个区间的第一棵树。若优先选最后一棵,如果不考虑前后两个区间选择的树的距离,虽然也能得到“能”的答案,但方案是不满足要求的,可以用 [X X X O] X [O X X X] 的数据卡掉;如果考虑前后两个区间选择的树的距离,则会得到“不能”的答案,也是错误的。
我没有考虑前后两个区间选择的树的距离,所以 [X X X O] X [O X X X] 这种数据会得到一个偏小的答案,然而可以 AC。
附 hack 数据:
1 2 10  5  4 2  2  2  5  1  5  1  1  2  10 
 
答案是 \(9\) ,我的代码会输出 \(8\) 。
 贪心算法代码 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include  <cstdio>  #include  <cstring>  #include  <algorithm>  #define  N 100005 int  n, m, k, a[N], h, t, Q[N];long  long  sum[N], id[N];void  push (int  x, long  long  *cp)  {     while  (h <= t && cp[Q[t]] <= cp[x]) --t;     Q[++t] = x; } bool  check (long  long  x)  {    h = 1 , t = 1 , Q[t] = k;     for  (register  int  i = 1 ; i <= n; i + k <= n ? push (i + k, sum) : void  (0 ), ++i){         if  (Q[h] < i) ++h;         if  (sum[Q[h]] - a[i] <= x) id[i] = i; else  id[i] = 0 ;     }     h = 1 , t = 0 ;     for  (register  int  i = 1 ; i <= k; ++i) push (i, id);     int  s = 0 ;     for  (register  int  i = 1 , p = 0 ; i <= n - k + 1 ; push (i + k, id), ++i){         if  (Q[h] < i) ++h;         if  (i <= p) continue ;         if  (sum[i + k - 1 ] <= x) continue ;         p = int  (id[Q[h]]), ++s;         if  (p == 0 ) return  0 ;     }     return  s <= m; } int  main ()  {    scanf ("%d%d%d" , &n, &m, &k);     for  (register  int  i = 1 ; i <= n; ++i) scanf ("%d" , a + i);     for  (register  int  i = 1 ; i <= n; ++i)         sum[i] = sum[i - 1 ] + a[i] - (i > k ? a[i - k] : 0 );      long  long  l = 0 , r = 100000000000000ll , md, ans;     while  (l <= r) if  (check (md = (l + r) >> 1 )) ans = md, r = md - 1 ; else  l = md + 1 ;     printf ("%lld\n" , ans); } 
 
 正确的解法 
一个正确但效率较低的二分 +DP 解法 
仍然求出 \(sum_i,mx_i\) ,考虑二分。
考虑用 \(O(nk)\)  的时间进行一个 DP 来判断。
首先考虑这样一个 DP 状态:\(dp_i\)  表示前 \(i\)  棵树(不包括 \(i\) ),满足所有长度为 \(k\)  的区间的和都不超过 \(x\) ,且第 \(i-k\)  棵树必砍 时,最少需要砍多少棵树。
考虑主动转移,枚举下一棵砍的树 \(i+j\ (j\ge 0)\) ,若可以砍 \(i+j\) (即满足 \(mx_{i+j}\le x\)  且 \(\max\limits_{i\le p< i+j} sum_p\le x\) ),则 \(dp_{i+j+k}\gets \min(dp_{i+j+k},dp_{i}+1)\) 。
然而直接这样做是 \(O(n^2)\)  的。通过观察发现,若 \(j\ge k\) ,则可以把 \(dp_i\)  先转移到 \(dp_{i+k}\) ,再把 \(dp_{i+k}\)  向前转移。于是,我们只需要考虑 \(0\le j< k\)  即可。时间复杂度优化为 \(O(nk)\) 。
总的时间复杂度为 \(O(nk\log ans)\) 。
另一个正确但效率较低的 DP 解法 
仍然求出 \(sum_i,mx_i\) ,但不二分,直接进行 DP。
\(dp_{i,j}\)  表示前 \(j\)  棵树,砍了 \(i\)  棵,\(j\)  必须砍时 \(\max\limits_{k\le p< j+k}sum_p\)  的最小值。
当 \(i\)  确定时,记 \(Mx_j\)  表示前 \(j\)  棵树(不包括 \(j\) ),砍了 \(i-1\)  棵树且最后一棵砍的树 \(t\)  满足 \(j-t\ge k\)  的 \(\max\limits_{k\le p< j} sum_p\)  的最小值。
其实求 \(Mx_j\)  的过程相当于一个 DP 的过程,显然有两种决策:当 \(t< j-k\)  时,\(Mx_j=\max(Mx_{j-1},sum_{j-1})\) ;当 \(t=j-k\)  时,\(Mx_j=dp_{i-1,j-k}\) 。所以有 \(Mx_j=\min(\max(Mx_{j-1},sum_{j-1}),dp_{i-1,j-k})\) 。
求出 \(Mx_j\)  后,求 \(dp_{i,j}\)  就变得很显然:\(dp_{i,j}=\max(Mx_j,mx_j)\) ,分别表示 \(\max\limits_{k\le p< j}sum_p\)  的最小值,以及 \(\max\limits_{j\le p< j+k}sum_p\)  的最小值,这两部分互不影响,所以可以独立计算。
求出 \(dp_{i,j}\)  后,求答案也变得很显然:由于 \(dp_{m,j}\)  并没有包括 \(\max\limits_{j+k\le p\le n}sum_p\) ,把这部分与 \(dp_{m,j}\)  取 \(\max\)  即可。
可以使用滚动数组优化内存。时间复杂度为 \(O(nm)\) 。
正解——两者之并 
可以发现,\(m> \lceil \frac{n}{k}\rceil\)  与 \(m= \lceil \frac{n}{k}\rceil\)  的情况答案是相同的。所以 \(m\)  最大为 \(\lceil \frac{n}{k}\rceil\) 。
上面的两个 DP,可以发现当 \(k\le 50\)  时第一个 DP 是可以过的。而 \(k>50\)  时,由于 \(m\)  的级别是 \(\frac{n}{k}\) ,第二个 DP 可以过。
那么把两者合并一下就可以通过此题。
 正解代码 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include  <cstdio>  #include  <cstring>  #include  <algorithm>  #define  N 100005 int  n, m, k, a[N], h, t, Q[N];long  long  sum[N], mx[N];void  push (int  x)  {     while  (h <= t && sum[Q[t]] <= sum[x]) --t;     Q[++t] = x; } namespace  Subtask1{     int  dp[N];     void  upd (int  i, int  j)  {         if  (i > n + 1 ) i = n + 1 ;         dp[i] = std :: min (dp[i], j);     }     bool  check (long  long  x)  {         for  (register  int  i = 1 ; i <= n + 1 ; ++i) dp[i] = 1e9 ;         dp[1 ] = 0 ;         for  (register  int  i = 1 ; i <= n; ++i){             long  long  Mx = 0 ;             for  (register  int  j = 0 ; j < k && i + j <= n; ++j){                 if  (Mx <= x && mx[i + j] <= x) upd (i + j + k, dp[i] + 1 );                 Mx = std :: max (Mx, sum[i + j]);             }                          if  (Mx <= x) upd (i + k, dp[i]);         }         return  dp[n + 1 ] <= m;     }     void  Main ()  {         long  long  l = 0 , r = 1e14 , md, ans;         while  (l <= r) if  (check (md = (l + r) >> 1 )) ans = md, r = md - 1 ; else  l = md + 1 ;         printf ("%lld\n" , ans);     } } namespace  Subtask2{     long  long  dp[2 ][N];     void  Main ()  {         long  long  Mx = 0 ;         for  (register  int  i = 1 ; i <= n; ++i){             if  (i > k) Mx = std :: max (Mx, sum[i - 1 ]);             dp[1 ][i] = std :: max (Mx, mx[i]);         }         for  (register  int  i = 2 ; i <= m; ++i){             int  t = i & 1 ;             long  long  Mx = 0 ;             for  (register  int  j = 1 ; j <= n; ++j){                 if  (j > k) Mx = std :: max (Mx, sum[j - 1 ]);                 if  (j > k) Mx = std :: min (Mx, dp[!t][j - k]);                 dp[t][j] = std :: max (Mx, mx[j]);             }         }         long  long  ans = 0x3f3f3f3f3f3f3f3f ll;         Mx = 0 ;         for  (register  int  i = n; i; --i){             if  (i + k <= n) Mx = std :: max (Mx, sum[i + k]);             ans = std :: min (ans, std :: max (dp[m & 1 ][i], Mx));         }         printf ("%lld\n" , ans);     } } int  main ()  {    scanf ("%d%d%d" , &n, &m, &k);     m = std :: min (m, n / k + 1 );     for  (register  int  i = 1 ; i <= n; ++i) scanf ("%d" , a + i);     for  (register  int  i = 1 ; i <= n; ++i)         sum[i] = sum[i - 1 ] + a[i] - (i > k ? a[i - k] : 0 );     h = 1 , t = 1 , Q[t] = k;     for  (register  int  i = 1 ; i <= n; i + k <= n ? push (i + k) : void  (0 ), ++i){         if  (Q[h] < i) ++h;         mx[i] = sum[Q[h]] - a[i];     }     if  (m <= 2005 ) return  Subtask2 :: Main (), 0 ;     if  (k <= 55 ) return  Subtask1 :: Main (), 0 ; }