P2511 [HAOI2008]木棍分割 题解

· · 题解

题解索引

  1. 题目大意
  2. Solution
  3. AC code
  4. 类似题型

代码类型: C++(cpp)

是否吸氧:否

不压行代码长度:38

题目大意

题面:<传送门>

题意:有 n 条线段,给出 n 条线段的长度。线段初始是连接在一起的,现要求你切断 最多 m 处,要求求出:

  1. 切割后,总长度最大且长度值最短 的值对 10007 取模的结果。

  2. 输出 满足第一问 的方法数。

术语理解:前缀和优化dp。

Solution

第一次用紫题骗咕值,好鸡冻

声明:本题解中有一些部分确实借鉴了楼上楼下大佬们的部分内容,不过还是可以保证部分的原创性 因为我不会写sigma,更不知道多个sigma连在一起对不对,有错见谅

首先,我们可以看到这道题有一个特点:

切割时,只能切 原有的分段连接处

我们按问题来吧,题目有两问,我们从第一问开始。

第一问:切割后,总长度最大且长度值最短的值对10007取模的结果

这个问题灰常简单,既然是找最合适的答案,无疑是 二分答案 最合适了。

做二分,首先确定数据所在的范围。第一问的答案范围无疑是 0 ~ (50000*1000) (这么点数据用模拟就行)

二分的基础思路就是假设,假设这个答案能行,代入进去算一遍看行不行。

我们假设的是最长长度,那么要求是首先不能有单一一段比他大的值,再一个就是分组的问题,分段的原则是 能少分就少分

最后分完段,再判断能不能分这么多段就行了。

第二问:输出满足第一问的方法数

其实说真的,楼上楼下的DALAO们都写了好多长串的公式,本人学问浅薄,不会写那些复杂的式子/fad。

n^3做法:黑 魔 法

按照楼上楼下大佬们写的 按照我们平常的思路,一般都会想到这个dp:

f[i][j] 表示前 i 根木棍分 j 组的方案数,

前提是 ```sum[i]-sum[k]<=ans1//ans1代表第一问的答案``` 。 其意义为枚举 $k$ 为分割点, $i$ 为木棍总数的枚举, $j$ 为分割点总数的枚举。 这里的 $m$ 是加了一的,因为割 $m$ 下分了 $m+1$ 段。 因其时间复杂度为 $O(n^3)$ 所以会 ~~学会黑魔法~~ T掉。 #### n*m做法:能 省 就 省 从上面的黑魔法我们了解到,靠三重循环是会T掉的。但当我们时间压到死之后又会发现, $f[50000][1000]$ 的数组是开不起的(我电脑太差还是)。 当然也可以用楼上楼下大佬的方法:用 ```short``` 建数组。 可我觉得这对数组不公平/dk 于是我写了一种只用一维数组去维护的方法。 首先,我们不论如何最终都是要求 $n$ 段分 $m+1$ 段,我们就可以直接压掉前面的那一维,没有必要了。 这样下来,都不需要滚动数组了。 ~~其实是我不会写~~ 好的好的,正式开始讲了: ~~根据题目标签~~ 我们可以先建一个前缀和数组,用于存储前缀和, ``` sum[i]表示分成i组的方案总数的前缀和 ``` 然后写出dp数组, ``` f[i]表示分成i组的方案总数 ``` 根据一维前缀和数组的原理(此处是例子,不是此题答案), ``` sum[i]-sum[i-1]表示i的值 ``` 但我们如果原样套上的话,是错误的(因为他并不是 $i$ 的前缀和,而是分成 $i$ 组的方案总数的前缀和,所以我们应该将数组下标改成分成 $i$ 组的,即改成分成 $i$ 组的方案总数的起始点和结束点)。 于是为便于计算分成 $i$ 组的方案总数的起始点,又一个新数组诞生了: ``` zuo[i]表示i如果靠左分组i所能到达的最远结点 ``` 我们统计最靠左的结点时需要看具体值才能找出最合适的值,但求数组中一段的和最快最简便的还是前缀和数组,于是我们又有了一个数组: ``` for(int i=1;i<=n;i++){ a[i]+=a[i-1];//将a数组变成前缀和数组,因为a数组已经没用了(其实是我懒) } ``` 然后对于每个不同的 $i$ , ``` while(a[i]-a[now]>ans1)now++; zuo[i]=now; ``` 这样就能找到合适的点了,那么我们计算分成 $i$ 组的方案总数就是: ``` sum[i]-sum[zuo[i]-1] ``` 然后我们初始化数组,注意 $sun[i]$ 首先先全部为 $1$ (因为 $f[i]$ 还没算),$f[i]$ 初始化为 $0$ 。 接着是友善的dp方程,然后是更新前缀和,记录答案 ```ans2``` 最后输出,这么愉快的结束了 当然还有一点我要讲,对于计算时的取模运算,一定要复制原题中的数字,不然很容易打错。 ~~要不是我因为10007写成了1007……这题解还能年轻一小时~~ ## AC code 首先说下,具体的代码注释我都写在里面了。拒绝抄袭,从我做起(主要懂了就能写出来,没必要抄代码,不然还拿个棕名) ```cpp #include<iostream> #include<cstdio> #define mods 10007 using namespace std; const int MAX=5e5+9; int n,m,a[MAX],ans1,ans2=0,sum[MAX],f[MAX],now=0,zuo[MAX]; bool check(int x){ /*********************** 思路:假设x成立,那么开始分组,每次当前组的总长度加上当前枚举的长度超过了x时,就重新分一个组(尽量少分组),最后分完看看这些组成不成立 ************************/ int len=0,num=1;//当前分组的总长度,当前总分的组数 for(int i=1;i<=n;i++){ if(a[i]>x)return false;//如果成立则x不成立 len+=a[i];//加入当前组 if(len>x)num++,len=a[i];//如果加入此组后大于了x,那么重新分一个组,并将a[i]作为新组的初始长度 }return num<=m;//看看这个最长是不是成立 }int main(){ scanf("%d%d",&n,&m);m++; for(int i=1;i<=n;i++){ scanf("%d",&a[i]); }int l=1,r=50000000; while(l<=r){ int mid=(l+r)/2; if(check(mid))ans1=mid,r=mid-1; else l=mid+1; }cout<<ans1<<" "; for(int i=1;i<=n;i++){ //a数组后面没用处了所以直接霍霍就行 a[i]+=a[i-1];//将a数组变成前缀和数组 while(a[i]-a[now]>ans1)now++;//寻找最左点 zuo[i]=now;//记录最左结点 }for(int i=0;i<=n;i++)f[i]=0,sum[i]=1;//初始化 zuo[0]=1;//因为后面zuo[0]-1变成-1,越界了,所以变成1 for(int i=1;i<=m;i++){//段数(砍i-1段) for(int j=1;j<=n;j++)f[j]=(sum[j-1]-sum[zuo[j]-1])%mods;//分成j块的分法总数等于j-1到zuo[j]的合理范围总数 if(i==1)sum[0]=0;////为了完善后一句,不让后一句出错 for(int j=1;j<=n;j++)sum[j]=f[j]+sum[j-1];//前缀和赋值(分成j块的分法总数的前缀和=分j块数的分法总数+分j-1块的前缀和) ans2=(ans2+f[n])%mods;//更新答案(砍法+=分n块的总数) }cout<<ans2<<endl; return 0; } ``` AC记录[<传送门>](https://www.luogu.com.cn/record/47413398) ## 类似题型 [P1043 [NOIP2003 普及组] 数字游戏](https://www.luogu.com.cn/problem/P1043)