bzoj 1044 [HAOI2008]木棍分割 题解

【原题】

1044: [HAOI2008]木棍分割

Time Limit: 10 Sec   Memory Limit: 162 MB
Submit: 1213   Solved: 411
[ Submit][ Status]

Description

有n根木棍, 第i根木棍的长度为Li,n根木棍依次连结了一起, 总共有n-1个连接处. 现在允许你最多砍断m个连接处, 砍完后n根木棍被分成了很多段,要求满足总长度最大的一段长度最小, 并且输出有多少种砍的方法使得总长度最大的一段长度最小. 并将结果mod 10007。。。

Input

输入文件第一行有2个数n,m. 接下来n行每行一个正整数Li,表示第i根木棍的长度.

Output

输出有2个数, 第一个数是总长度最大的一段的长度最小值, 第二个数是有多少种砍的方法使得满足条件.

Sample Input

3 2
1
1
10

Sample Output

10 2

两种砍的方法: (1)(1)(10)和(1 1)(10)
数据范围
n<=50000, 0<=m<=min(n-1,1000).
1<=Li<=1000.


【序言】这道题磨了一个上午。总算过了~~

【分析一】第一问真心的简单。直接二分枚举最大长度的最小值即可。细节注意一下。

bool check(int maxx)
{
  int now=0,t=1;
  for (int i=1;i<=n;i++)
    if (now+a[i]<=maxx) now+=a[i];
    else 
    {
      if (a[i]>maxx||t>=m) return false;
      t++;now=a[i];
    }
  return true;
}
int erfen(int l,int r)
{
  if (l==r) return l;
  int mid=(l+r)/2;
  if (check(mid)) return erfen(l,mid);
  return erfen(mid+1,r);
}

【分析二】第二问很容易看出是DP,而且方程也很简单。设f[i][j]表示到第i个点,截取j个木棍且满足要求的方案数。为了加快速度,我们开一个前缀和的sum优化,那么i--j的长度就是sum[j]-sum[i-1]。

for (i=2;i<=n;i++)
    for (j=2;j<=m;j++) 
      for (k=1;k<i;k++)
        if (sum[i]-sum[k]<=ans) f[i][j]=(f[i][j]+f[k][j-1])%p;

然后惊奇地发现:时间、空间都不符合要求!!我们来一点一点优化。

【优化一*空间】这个相对简单啊。我们通过观察发现,f[i][j]中的j只和j-1有关,所以可以用滚存。为了方便,我们可以把i,j的位置换一下,然后把j的循环提到最上面。下面的滚存代码中,now就是用来滚存的。

for (j=2;j<=m;j++) 
  {
    now^=1;
    memset(f[now],0,sizeof(f[now]));
    for (i=n;i>1;i--)
      for (k=1;k<i;k++)
        if (sum[i]-sum[k]<=ans) f[now][i]=(f[now][i]+f[now^1][k])%p;  
    ans2=(ans2+f[now][n])%p;
  }

其中的now^1就是0和1的变换。这样,就不用开50000*50000的数组了!空间过得去了。

【优化二*时间】想了很长时间。同样也是观察方程。每次k都要循环一边,是不是很无用啊!试想:若sum[i]-sum[k]<=ans,那么sum[i-1]-sum[k]<=ans(其中i-1>k)(因为sum数组是前缀和,满足单调递增)。因此我们可以用类似于单调队列的方法,开一个后缀数组g。如果sum[i]-sum[k]<=ans,就把计算后的值赋到g[i]中去。在以后每个小于i的数中,若i>k,i就可以加上g数组。(有点说不清楚,具体还是要自己仔细研究)

for (j=2;j<=m;j++) 
  {
    now^=1;
    memset(f[now],0,sizeof(f[now]));
    memset(g,0,sizeof(g));k=n+1;
    for (i=n;i>1;i--)
    {
      if (i>k) f[now][i]+=(g[k]-g[i])%p;else k=i;
      while (k>1&&sum[i]-sum[k-1]<=ans)
      {
        k--;
        f[now][i]=(f[now][i]+f[now^1][k])%p;
        g[k]=(g[k+1]+f[now^1][k])%p;
      }
    }
    ans2=(ans2+f[now][n])%p;
  }
【小结】加了这些优化之后,bzoj上就过的去了。但是代码写的有点乱啊。真是一道经典的DP优化!

【总的AC代码】

#include<stdio.h>
#include<cstring>
using namespace std;
const int maxn=50000+5;
const int p=10007;
int f[2][maxn],n,m,ans,ans2,sum[maxn],a[maxn],i,j,k,now,g[maxn];
bool check(int maxx)
{
  int now=0,t=1;
  for (int i=1;i<=n;i++)
    if (now+a[i]<=maxx) now+=a[i];
    else 
    {
      if (a[i]>maxx||t>=m) return false;
      t++;now=a[i];
    }
  return true;
}
int erfen(int l,int r)
{
  if (l==r) return l;
  int mid=(l+r)/2;
  if (check(mid)) return erfen(l,mid);
  return erfen(mid+1,r);
}
int main()
{
  scanf("%ld%ld",&n,&m);m++;
  for (i=1;i<=n;i++)
  {
    scanf("%ld",&a[i]);
    sum[i]=sum[i-1]+a[i];
  }
  ans=erfen(0,sum[n]);
  printf("%ld ",ans);
  for (i=1;i<=n;i++) 
    if (sum[i]<=ans) f[1][i]=1;else break;
  now=1; 
  for (j=2;j<=m;j++) 
  {
    now^=1;
    memset(f[now],0,sizeof(f[now]));
    memset(g,0,sizeof(g));k=n+1;
    for (i=n;i>1;i--)
    {
      if (i>k) f[now][i]+=(g[k]-g[i])%p;else k=i;
      while (k>1&&sum[i]-sum[k-1]<=ans)
      {
        k--;
        f[now][i]=(f[now][i]+f[now^1][k])%p;
        g[k]=(g[k+1]+f[now^1][k])%p;
      }
    }
    ans2=(ans2+f[now][n])%p;
  }
  printf("%ld",ans2);
  scanf("%ld%ld",&n,&m);
  return 0;
}

你可能感兴趣的:(题解,dp,单调队列,bzoj)