BZOJ1044: [HAOI2008]木棍分割
【传送门:BZOJ1044】
简要题意:
给出n个数,求出最多分成m+1段的最长段的最小值,并且求出能分成最长段最小的情况数
题解:
一道思维题(好吧,就是搞了我一晚上的题)
首先最小值我们可以用二分来搞出来,二分最小值,然后从头开始,一直累加,如果当前累加值加上a[i]超过了二分出来的值的话,就新开一个段,并且把a[i]放在这个段的开头,最后如果分成的段小于等于m+1的话,就证明这个最小值是行得通的
其实求最小值并不是难处,关键是怎么求情况数
很容易想到设f[i][j]为到第i个数时分成j段的情况数
设s[i]为$\sum_{j=1}^{i}a[j]$
可以得到方程$$f[i][j]=\sum_{s[i]-s[k]<=d}(i>k)f[k][j-1]$$
但是这样O(mn^2)肯定会T,而且空间也过大
那么怎么优化呢?
我们发现其实f[][j]只与f[][j-1]有关系,那么我们可以用滚动数组来优化空间
然后其实a数组和s数组是保持不变的,也就是说对于i这个位置而言,往前面延伸,能延伸最前面的位置设为k[i],也就是当s[i]-s[k]<=d时,k[i]就表示为k的最小值
那么我们就可以把方程转化为$$f[i][q]=\sum_{s[i]-s[k]<=d}(i>k)f[k][p]$$
p表示上一次的状态,q表示现在要处理的状态
其实我们看得出来可以用前缀和来得到$\sum_{s[i]-s[k]<=d}(i>k)f[k][p]$
时间就可以优化到O(nm)了
接下来是些小细节:对于一开始f[0][0]要等于1,因为后面的数有可能只分成一段,所以用来特判这种情况
参考代码:
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int l[51000],m,n; int s[51000]; int sum[51000]; int f[2][51000]; int k[51000]; bool check(int d) { int dd=0;int mm=1; for(int i=1;i<=n;i++) { if(dd+l[i]>d) { dd=l[i]; if(dd>d) return false; mm++;if(mm>m) return false; } else dd+=l[i]; } return true; } int main() { scanf("%d%d",&n,&m);m++; s[0]=0; for(int i=1;i<=n;i++) { scanf("%d",&l[i]); s[i]=s[i-1]+l[i]; } int l=1,r=s[n]; int d=0; while(l<=r) { int mid=(l+r)/2; if(check(mid)==true) { d=mid; r=mid-1; } else l=mid+1; } printf("%d ",d); memset(f,0,sizeof(f)); l=1;r=1; while(r<=n) { while(s[r]-s[l-1]>d) l++; k[r]=l; r++; } int p=0; int ans=0; f[0][0]=1; for(int i=1;i<=m;i++) { int q=1-p; sum[0]=f[p][0]; for(int j=1;j<=n;j++) sum[j]=(sum[j-1]+f[p][j])%10007; f[q][0]=0; for(int j=i;j<=n;j++) { int d; if(k[j]==1) d=sum[j-1]; else d=sum[j-1]-sum[k[j]-2]; f[q][j]=(d+10007)%10007; } ans=(ans+f[q][n])%10007; p=q; } printf("%d\n",(ans+10007)%10007); return 0; }
渺渺时空,茫茫人海,与君相遇,幸甚幸甚