1 条题解

  • 0
    @ 2025-8-24 21:24:51

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar liangbowen
    不能再摆了,,,

    搬运于2025-08-24 21:24:50,当前版本为作者最后更新于2022-06-20 18:12:59,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    前言

    题目传送门!

    更好的阅读体验?

    这题是一道挺好的 dp\texttt{dp} 题啊,但大家的题解都写得不够详细。

    所以,我来补一篇 LaTeX\LaTeX 题解,希望能帮助大家。

    思路

    首先是读入,为了方便,我让字符串下标从 11 开始。

    string a;
    int n; //字符串长度。
    void Input()
    {
    	cin >> a;
    	//个人习惯将起点下标变为 1 来算。
    	n = a.length();
    	a = '@' + a;
    }
    

    为了方便,我们后面用 num(x,y)num(x, y) 表示下标为 [x,y][x, y] 所构成的数。

    容易想到,题目就是要我们求出:任意一个位置的最大结束下标

    考虑到,算这个之前,我们还得先算一算:任意一个位置的最小开始下标

    具体地,设 fif_i 表示将前 ii 位拆成递增数,最后一个数最小num(fi,i)num(f_i, i)

    想要最小,则 fif_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} $$

    这里,我们发现一个问题:如何比较两个数的大小?

    题解里的解决办法,代码稍长,提供一种简单的比较方式。

    1. 分离出 num(x,y)num(x, y),注意要去除前导 00
    2. 比较两个数(注意两个数实际上使用 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; //如果没有符合的,则从头开始算一个。 
    	}
    }
    

    接下来,就是处理『最小开始下标』了。

    dpidp_i 表示将 [i,n][i, n] 这一段拆成递增数,第一个数最大num(i,fi)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} $$

    并且有 dpf[n]=ndp_{f[n]} = n

    但是,这里并没有那么简单!

    如果你直接就这样打了,可能只会获得 90pts\texttt{90pts}

    错误原因是后面的 00 造成的。

    举例来说,有一个数 1234050\texttt{1234050}

    如果单纯这样做,结果为 1,2,3,40,50\texttt{1,2,3,40,50}

    发现 f7=6f_7 = 6,所以初始化 dp6=7dp_6 = 7

    但是,第 55 位是 0\texttt{0},根据样例,050\texttt{050} 也是符合的,并且末尾元素仍然最小!

    显然,保持最优解的情况下,位数多一点更好。

    所以,我们应该也让 dp5=7dp_5 = 7

    这样,结果为 12,34,050\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)O(n),实际运行时可以理解为常数。
    • 两次 DP\texttt{DP} 的时间复杂度都是 O(n2×n=n3)O(n^2 \times n = n^3),实际运行接近 O(n2)O(n^2)

    码字不易,感谢大家观看,希望能帮助到大家!

    • 1

    信息

    ID
    409
    时间
    1000ms
    内存
    125MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者