题目传送门!
更好的阅读体验?
这题是一道挺好的 \(\texttt{dp}\) 题啊,但大家的题解都写得不够详细。
所以,我来补一篇 \(\LaTeX\) 题解,希望能帮助大家。
首先是读入,为了方便,我让字符串下标从 \(1\) 开始。
string a; int n; //字符串长度。 void Input() { cin >> a; //个人习惯将起点下标变为 1 来算。 n = a.length(); a = '@' + a; }
为了方便,我们后面用 \(num(x, y)\) 表示下标为 \([x, y]\) 所构成的数。
容易想到,题目就是要我们求出:任意一个位置的最大结束下标。
考虑到,算这个之前,我们还得先算一算:任意一个位置的最小开始下标。
具体地,设 \(f_i\) 表示将前 \(i\) 位拆成递增数,最后一个数最小为 \(num(f_i, i)\)。
想要最小,则 \(f_i\) 要尽可能大。
我们可以画一个图,方便推状态转移方程。
所以,状态转移方程如下。
\[f_i = \max\limits_{j=1}^{i}\begin{cases}j & num(f_{j-1}, j-1) < num(j, i)\\1 & j = 1\end{cases} \]这里,我们发现一个问题:如何比较两个数的大小?
题解里的解决办法,代码稍长,提供一种简单的比较方式。
string
存储的):
这样代码会短很多。
string num(int x, int y) //将 a[i]~a[j] 的数分割出来,去除前导 0。 { string s = a.substr(x, y-x+1); while (s.length() > 1 && s[0] == '0') s.erase(0, 1); return s; } bool Less(string x, string y) //比较 x 与 y 两个数,注意不是比较字典序。 //当且仅当 x < y 返回 true。 { if (x.length() != y.length()) return x.length() < y.length(); return x < y; //长度相等,则字典序比较等同于数的比较。 } bool cmp(int x1, int y1, int x2, int y2) //等同于:比较 num(x1, y1) 与 num(x2, y2) 的大小。 { string t1 = num(x1, y1), t2 = num(x2, y2); return Less(t1, t2); }
有了这个 cmp()
函数,我们就可以很方便地完成第一次动规。
int f[N]; void DP1() { for (int i = 1; i <= n; i++) { for (int j = i; j >= 2; j--) //从后往前枚举,第一次枚举到的 j 一定是最大的。 if (cmp(f[j-1], j-1, j, i)) { f[i] = j; break; } if (f[i] == 0) f[i] = 1; //如果没有符合的,则从头开始算一个。 } }
接下来,就是处理『最小开始下标』了。
设 \(dp_i\) 表示将 \([i, n]\) 这一段拆成递增数,第一个数最大为 \(num(i, f_i)\)。
我们再画一张图。
所以,状态转移方程如下。
\[dp_i = \max\limits_{j = i}^{f_n - 1}\begin{cases}j & num(i, j) < num(j+1, dp_{j+1})\end{cases} \]并且有 \(dp_{f[n]} = n\)。
但是,这里并没有那么简单!
如果你直接就这样打了,可能只会获得 \(\texttt{90pts}\)。
错误原因是后面的 \(0\) 造成的。
举例来说,有一个数 \(\texttt{1234050}\)。
如果单纯这样做,结果为 \(\texttt{1,2,3,40,50}\)。
发现 \(f_7 = 6\),所以初始化 \(dp_6 = 7\)。
但是,第 \(5\) 位是 \(\texttt{0}\),根据样例,\(\texttt{050}\) 也是符合的,并且末尾元素仍然最小!
显然,保持最优解的情况下,位数多一点更好。
所以,我们应该也让 \(dp_5 = 7\)。
这样,结果为 \(\texttt{12,34,050}\)。
代码比较好打,只需要注意此处的判断即可。
int dp[N]; void DP2() { dp[f[n]] = n; //关键步骤,增添最后一个数的前导 0。 int pos = f[n]; for (pos = f[n]; pos-1 && a[pos-1] == '0'; pos--) dp[pos-1] = n; for (int i = pos-1; i; i--) //剩下的就和 DP1() 基本相等。 for (int j = f[n] - 1; j >= i; j--) if (cmp(i, j, j+1, dp[j+1])) { dp[i] = j; break; } }
大功告成,最后输出比较容易了。
void Output() { string ans = ""; //本质就是输出了,只不过要处理末尾逗号罢了。 for (int i = 1; i <= n; i = dp[i] + 1) { for (int j = i; j <= dp[i]; j++) ans += a[j]; ans += ','; } ans.erase(ans.length()-1, 1); //擦除最后一位逗号。 cout << ans; }
其实上面就是代码了,组合在一起即可。
但还是给出完整代码。具体注释参照上面。
这份完整代码保留的注释是调试语句,大家可以看一下。
码量不大。
#include <iostream> #include <cstdio> #define N 505 using namespace std; string a; int n; void Input() { cin >> a; n = a.length(); a = '@' + a; } string num(int x, int y) { string s = a.substr(x, y-x+1); while (s.length() > 1 && s[0] == '0') s.erase(0, 1); return s; } bool Less(string x, string y) { if (x.length() != y.length()) return x.length() < y.length(); return x < y; } bool cmp(int x1, int y1, int x2, int y2) { string t1 = num(x1, y1), t2 = num(x2, y2); return Less(t1, t2); } int f[N]; void DP1() { for (int i = 1; i <= n; i++) { for (int j = i; j >= 2; j--) if (cmp(f[j-1], j-1, j, i)) { f[i] = j; break; } if (f[i] == 0) f[i] = 1; //printf("dp[%d] = %d.\n", i, dp1[i]); } /* for (int i = 1; i <= n; i++) printf("f[%d] = %d.\n", i, f[i]); printf("\n"); */ } int dp[N]; void DP2() { dp[f[n]] = n; int pos = f[n]; for (pos = f[n]; pos-1 && a[pos-1] == '0'; pos--) dp[pos-1] = n; //printf("pos = %d.\n", pos); for (int i = pos-1; i; i--) for (int j = f[n] - 1; j >= i; j--) if (cmp(i, j, j+1, dp[j+1])) { dp[i] = j; //printf("dp[%d] = %d.\n", i, dp2[i]); break; } //for (int i = 1; i <= n; i++) printf("dp[%d] = %d.\n", i, dp[i]); } void Output() { string ans = ""; for (int i = 1; i <= n; i = dp[i] + 1) { for (int j = i; j <= dp[i]; j++) ans += a[j]; ans += ','; } ans.erase(ans.length()-1, 1); cout << ans; } int main() { Input(); DP1(); DP2(); Output(); return 0; }
建议大家好好复盘一下状态转移方程。
顺便给出时间复杂度:
cmp()
函数的时间复杂度大约是 \(O(n)\),实际运行时可以理解为常数。码字不易,感谢大家观看,希望能帮助到大家!
首发:2022-06-20 18:12:59