题解 P4302 【[SCOI2003]字符串折叠】

kradcigam

2019-08-28 11:33:24

Solution

- Update 2020-6-5:感谢 @[blanc_x](https://www.luogu.com.cn/user/101620) 帮忙检查出了一处笔误。 - Update 2020-8-7:感谢 @[Garrison](https://www.luogu.com.cn/user/154334) 指出错误。 - Update 2021-8-12:修改优化了排版&解答评论问题: - 关于 4 重循环:包含函数里的一重,请不要再问这样的问题了。 - Update 2021-10-16:感谢 @[0Arctic0](https://www.luogu.com.cn/user/367991) 帮助修改了时间复杂度部分,增强语言的严谨性。 --- ## 讲讲我的做法 题目大意:对一个字符串进行折叠是它长度最小。 看一眼数据范围:哇!字符串长度不超过 **100**!这是一道省选题,不可能给你太宽裕的时限,所以,题目基本暗示你要用 $n^{3}$ 多一些的算法复杂度。 这是一道最优化的题目,常见求最优化问题的算法比如贪心,模拟,枚举我都想不出什么好办法,唯独觉得像一道区间 dp。 ### 区间 dp 的分析 #### 解释状态 我们用 $f_{i,j}$ 表示 $i\sim j$ 这个区间内最小的长度。 首先,我们可以把 $i\sim j$ 这个区间的字符串拆成 2 部分处理。 就有了这段代码: ```cpp for(int l=2;l<=n;l++) for(int i=1,j=i+l-1;j<=n;i++,j++) for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); ``` 当然我用了字符串,然后加空格,这样更加符合人脑思维。 也有同学喜欢用字符数组,我也写了这样的一段代码: ```cpp for(int l=2;l<=n;l++){ for(int i=0,j=i+l-1;j<n;i++,j++){ for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); } } ``` #### 折叠 至于如何判断能否折叠,我呢用了一个函数——`check`,来检查一下是否可以折叠。 字符串代码: ```cpp bool check(int l,int r,int len){ for(int i=l;i<=r;i++) if(st[i]!=st[(i-l)%len+l])return false; return true; } ``` 字符数组代码: ```cpp bool check(char s[],int n,int len){ for(int i=len;i<n;i++) if(s[i]!=s[i%len])return false; return true; } ``` 判断好了是否可以折叠,我们就可以去写状态了,从 $i\sim j$,判断区间折叠的循环节。 字符串代码: ```cpp for(int l=2;l<=n;l++){ for(int i=1,j=i+l-1;j<=n;i++,j++){ for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); for(int k=i;k<j;k++){ int len=k-i+1; if(l%len!=0)continue; if(check(i,j,len))f[i][j]=min(f[i][j],f[i][k]+2+m[l/len]); } } } ``` 字符数组代码: ```cpp for(int l=2;l<=n;l++){ for(int i=0,j=i+l-1;j<n;i++,j++){ for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); for(int k=i;k<j;k++){ int len=k-i+1; if(l%len!=0)continue; if(check(s+i,l,len))f[i][j]=min(f[i][j],f[i][k]+2+m[l/len]); } } } ``` #### 边界条件以及初始化 刚刚的代码里出现里 $m$,现在我就来解释一下 $m$ 数组是干什么的。 **$m_i$ 的值表示的是数字 $i$ 的位数**,因为字符串的长度跟数字的位数有关。 我用的是最简单的方法,`for` 循环扫,**注意**:**100 也要赋值,万一数据给你 100 个同样的字符**。 ```cpp for(int i=1;i<=9;i++)m[i]=1; for(int i=10;i<=99;i++)m[i]=2; m[100]=3; ``` 现在我们想一想初始化怎么做? 显然,$f_{i,i}=1$,其他初值设为 $∞$。 ```cpp memset(f,0x3f,sizeof(f)); for(int i=1;i<=n;i++)f[i][i]=1; ``` 现在我们已经做完了所有的步骤,让我们看一看完整代码吧。 字符串代码: ```cpp #include <bits/stdc++.h> using namespace std; string st; int n,m[110],f[110][110]; bool check(int l,int r,int len){ for(int i=l;i<=r;i++) if(st[i]!=st[(i-l)%len+l])return false; return true; } int main(){ cin>>st; n=st.size(); st=' '+st; for(int i=1;i<=9;i++)m[i]=1; for(int i=10;i<=99;i++)m[i]=2; m[100]=3; memset(f,0x3f,sizeof(f)); for(int i=1;i<=n;i++)f[i][i]=1; for(int l=2;l<=n;l++){ for(int i=1,j=i+l-1;j<=n;i++,j++){ for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); for(int k=i;k<j;k++){ int len=k-i+1; if(l%len!=0)continue; if(check(i,j,len))f[i][j]=min(f[i][j],f[i][k]+2+m[l/len]); } } } printf("%d",f[1][n]); return 0; } ``` 字符数组代码: ```cpp #include <bits/stdc++.h> using namespace std; char s[110]; int n,m[110],f[110][110]; bool check(char s[],int n,int len){ for(int i=len;i<n;i++) if(s[i]!=s[i%len])return false; return true; } int main(){ scanf("%s",s); n=strlen(s); for(int i=1;i<=9;i++)m[i]=1; for(int i=10;i<=99;i++)m[i]=2; m[100]=3; memset(f,0x3f,sizeof(f)); for(int i=0;i<n;i++)f[i][i]=1; for(int l=2;l<=n;l++){ for(int i=0,j=i+l-1;j<n;i++,j++){ for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); for(int k=i;k<j;k++){ int len=k-i+1; if(l%len!=0)continue; if(check(s+i,l,len))f[i][j]=min(f[i][j],f[i][k]+2+m[l/len]); } } } printf("%d",f[0][n-1]); return 0; } ``` ### 时间复杂度 看上去我们套了 $4$ 个循环,然而真的时间复杂度就达到了 $n^{4}$ 吗?其实不是的。 首先 $n^{3}$ 肯定是存在的,那么为什么时间复杂度没有达到 $n^{4}$ 呢! 原因在于我们的 `continue`,它的复杂度是 $\log n$。 为什么? 我们进行 `check` 操作的显然是 $l$ 的因数,而 $l$ 的因数个数$<\log{l}$。 现实当中的常数还会更小,因为 `check` 的常数很小,它不是从 $1$ 开始,也没有到 $n$ 结束,并且一旦发现错误后会直接 `return`。 其实可以把里面的 2 个循环并成一个循环,但为了让大家看的更清楚,就不演示了。