【DP_区间DP专辑】

区间动态规划详解与经典案例

     区间DP是一类在区间上进行动态规划的最优问题,一般是根据问题设出一个表示状态的dp,可以是二维的也可以是三维的,一般情况下为二维。然后将问题划分成两个子问题,也就是一段区间分成左右两个区间,然后将左右两个区间合并到整个区间,或者说局部最优解合并为全局最优解,然后得解。

    这类DP可以用常规的for循环来写,也可以用记忆化搜索来写,个人更倾向于记忆化搜索写法,因为这种写法相当易懂,要什么值你直接去记忆化搜索一下就ok了,随叫随到随用啊。


    1、Zoj 3537 Cake

    比较经典的三角剖分。

    状态转移方程:dp[i][j] = min(dp[i][k]+dp[k][j]+cost[i][k]+cost[k][j]),(j >= i+ 3,i+1<=k<=j-1,cost[i][k]为连一条i到k的线的费用)  

    详细报告见Here


    2、Light oj 1422 Halloween Costumes

    很基础的区间DP,是不老传说那题的减弱版。

    状态转移方程:dp[i][j] = dp[i][j-1] (arr[i] == arr[j])

                            dp[i][j] = min(dp[i][k]+dp[k+1][j]) (arr[i] == arr[k] && i <= k <= j)


     3、poj 2955 Brackets

     经典的区间DP模型--最大括号匹配数。如果找到一对匹配的括号[xxx]oooo,就把区间分成两部分,一部分是xxx,一部分是ooo,然后以此递归直到区间长度为1或者为2.

     状态转移方程:dp[i][j] = min(dp[i+1][j],dp[i+1][k-1]+dp[k+1][j]+1)(i<=k<=j&&i和k是一对括号)


     4、CF 149 D Coloring Brackets

     限制很多的括号匹配类区间DP,因为给定的是一个合法序列,那么左括号对应着右括号的位置就定下来了,那么[i,j]区间就分成了[i+1,match[i]-1]和[match[i]+1,j],再记录下某个区间左右两边的颜色就好了。 


    5、1651 Multiplication Puzzle

     经典的区间DP模型--矩阵链乘的变换。我们从一开始一步步往后删很困难进行DP,那么从最后只剩两个也就是最左和最右时开始一步步增加就成功转换为经典的矩阵链乘了。一开始的区间是[i,j],那么我们可以选择i<k<j进行转移,就是用这个k将区间划分成两段,然后这两段再往下递归,最后返回信息,最后的最后返回的[i,j]的最少乘积和。

     状态转移:dp[i][j] = min(dp[i][k]+dp[k][j]+arr[i]*arr[k]*arr[j]);


     6、Zoj 3469 Food Delivery

    有n个人叫餐,每个人都在x轴上,并且每个人都有个坑爹度(和等餐时间有关,据说顾客认为坑爹值到一定程度他的小宇宙就要爆发).现在送餐员从x轴上的某点出发,路上奔跑速度是v,要一次性把所有餐送完。叫餐的人得到餐的时间和顺序不同,坑爹度总和也就不同。合格的送餐员要让客户体验最好,请问最少坑爹度和为多少。先YY出一个结论,最优的送餐顺序肯定是餐馆往两边来回跑,然后就可以进行状态转移了.

     详细报告见Here.   


    7、Hdu 4283 You Are the One  (较难)

    题意是给定一个序列,序列内的人有屌丝值Di,是第然后将这个序列进栈,第i个人如果是第k个出栈,那么最后的屌丝总值增加Di * (k-1), 求一个出栈序列使得总屌丝值最小。看这题目很难想到区间DP吧?是的,很不像,这样的题目是好题目。其实一个合法的出栈序列有一个性质:如果第i个人第k个出栈,那么在i个之后的人不可能会比i更早,这样就找到一个划分关系,k以前位一段,k以后为一段。常规的状态转移方程:dp[i][j] = min(dp[i+1][k]+dp[k+1][j]+arr[i]*(k-i)+(k-i+1)*(sum[j]-sum[i])) (i<=k<=j).详细报告见Here.


     8、Sdut 不老的传说问题 (较难)

      刚看到题目的时候一点想法都没有,关于颜色的状态怎么表示,莫非要开一维来表示颜色,可是这样转移就很怪异了,或者倒着推,看刷几下全变成0,这似乎也不行。一筹莫展额时候突然想到根本不需要表示颜色,只需要表示某个位置是不是刷到了相应的颜色即可.5 4 3 1 2 1 2 1 3 4 5,假设5 4 3和3 4 5都刷到位了,那么1 2 1 2 1这四个位置上可能已经有其他颜色,但在我们看来它和无色一样,因为它不是最终的颜色。单独考虑1 2 1 2 1,这要怎么刷呢,由于最后的那个1和一开始那个1一样,如果他们之间距离小等于k,那么我们就可以只计算1 2 1 2。如果距离大于k,那么我们可以把这段区间分成1,2 1 2 1,或者1 2 1 , 2 1这两种方案中刷得次数较小的就是1 2 1 2 1需要刷得次数.以此递归下去,便可得解。因为本题是个圈,所以需要用到一个技巧,那就是把1.2...n-1复制到n+1,...2 * n - 1,然后最终的答案就是min(dp[i][i+n-1])(1<=i<=n)

     状态转移方程:dp[i][j] = dp[i][j-1] ; (i + k - 1 >=j && arr[j] == arr[i])

                           dp[i][j] = min(dp[i][k]+dp[k+1][j]) (1<=k<=min(i+k-1,j) && arr[i] == arr[k])

     初始化dp[i][i] = 1,dp[i][j] = INF (j > i)


     9、Hdu String painter

     2008年长春区域赛的题目,和上一题很像,但觉得比上一题简单。由于每次可以选择将某段刷成某个字符a,然后下次刷得时候,假设刷b,可以把这段刷成不一样的两段,前一段时b,后一段是a。大概的刷法就是这样,这个b其实是s2[i]。 

     状态转移方程很容易想到:dp[i][j][pre] = min(dp[i+1][k][s2[i]] + dp[k+1][pre]) (pre == s2[i] && i <= k <= j)

                                            dp[i][j][pre] = min(dp[i+1][k][s2[i]] + dp[k+1][pre] + 1) (pre != s2[i] && i <= k <= j)



     这类DP的写法一般是固定,两种写法可以参照下这篇文章里的代码:http://blog.csdn.net/woshi250hua/article/details/7973824

      本文ZeroClock原创,但可以转载,因为我们是兄弟。

这是之前的思考过程,请继续补全: 题目要求:给定一个数字字符串(可能很长,所以用字符串处理),我们可以在任意两个字符之间插入加号或者不插入,形成表达式。但要求每个分割出的数字不能有前导零(除非本身就是0,但题目要求加数大于0,所以不可能有0,因此实际上每个数字不能以'0'开头,除非是一位数的0,但题目要求加数大于0,所以每个数字至少为1,因此不能出现以0开头的多位数)。 需要计算: 1. 方案数(即合法的插入加号的方式) 2. 所有方案中,表达式的最大值、最小值,以及所有方案表达式的总。 注意:表达式可以是整个数字(不加任何加号)。 例如:"1023"有4种方案:1023, 10+23, 102+3, 10+2+3。 但是注意:10+23中的"023"是不合法的,因为"023"有前导零。然而在样例1中,10+23是合法的,因为23没有前导零。而01这种是不合法的。 因此,在分割时,如果某一段的第一个字符是'0'且这一段长度大于1,那么就是非法的。 思路: 我们可以使用动态规划或者深度优先搜索(DFS)来枚举所有可能的分割位置。由于数字长度最大可能达到18(从数据范围看,测试点9~10是10^18,所以字符串长度最大18),因此我们可以用状态压缩或者直接DFS+记忆化,或者动态规划。 由于长度最多18,我们可以用O(2^(n-1))的枚举,即枚举每个间隔(共n-1个间隔)是否插入加号,然后检查每个分割出来的数字是否合法(无前导零,除非单个0,但题目要求加数大于0,所以单个0也不允许?注意题目要求:数字不能包含前导0,并且加数大于0。所以如果遇到0,那么只能单独作为一个数?但是题目要求加数大于0,所以0是不允许的。因此,如果字符串中有0,那么它只能出现在一个非零数字后面吗?不对,实际上题目要求每个加数都是正整数,所以不能有0(包括单个0也不允许)。因此,在分割时,一旦某一段第一个字符是'0',那么这一段就不合法(因为长度大于1,或者长度为1但是0也是不允许的)。 所以,在枚举分割时,需要跳过那些以0开头的多位数(即长度>1且第一个字符为'0')的段,以及单独一个0也是不允许的(因为题目要求加数大于0)。 因此,我们需要在枚举时检查每一段是否合法:如果这一段的第一个字符是'0'且长度大于1,则非法;否则(即第一个字符不是'0',或者长度等于1且是0?但0不允许,所以实际上我们要求每一段都不能以0开头(除非是0本身,但是0又不允许,所以干脆不允许任何一段包含0?不对,比如10,如果我们分割成10,那么0是不允许的。所以实际上,每一段必须满足:要么是一个大于0的数且没有前导零(即如果长度大于1,则第一个字符不能是'0';如果长度等于1,可以是0吗?不行,因为题目要求加数大于0,所以任何一段都不能是0(包括单个0))。 因此,在分割时,每一段必须满足:第一个字符不是'0'(那么长度大于1时没有前导零)且整个段代表的数字大于0(自然满足,因为第一个字符不是0,且至少是一位数)。注意:如果整个字符串就是"0"呢?但题目要求加数大于0,所以输入不会是0?但是题目没有明确说明,但是根据样例,输入都是正整数,所以我们可以认为输入字符串第一个字符不是0(除非长度为1的0)。但是题目数据范围n≤10^18,所以最小为1,所以不会出现0。 所以我们只需要检查每一段:如果该段长度大于1且第一个字符是'0',则跳过;或者该段为"0"(单独一个0)则跳过(因为0不允许)。但实际上,因为输入字符串可能包含0吗?比如"1023",其中就包含0,但是分割时不能把0单独作为一段(因为0不允许),所以分割时遇到0开头的段且长度大于1,我们直接跳过。但是注意:如果一段是"0",即单独一个0,那么也是不允许的,所以也要跳过。 然而,整个表达式就是一个数字,这个数字可以是0吗?题目要求加数大于0,所以整个表达式如果是0,那么也是不允许的?但是题目说“不加入加号也可以”,所以整个表达式就是一个数字,这个数字必须大于0(因为题目要求加数大于0,所以这个数字也必须大于0)。因此,如果整个字符串是"0",那么没有合法方案。但是题目数据范围是正整数,所以不考虑0。 所以,我们枚举所有分割方案(2^(n-1)种),然后检查每一段是否合法(每一段第一个字符不能是'0'(这样保证了没有前导零,且长度大于1时不会出现0开头,同时也保证了不会出现单独0的情况,因为单独0的第一个字符就是0,所以会被排除))。 但是,注意:一段如果是"0"(单独一个0)是不允许的,而如果一段是"01"(有前导零)也是不允许的。而我们的检查:如果第一个字符是'0',那么不管后面是什么,只要长度大于1就不允许;而如果长度等于1且是0,那么也是第一个字符为0,所以也会被检查到。因此,我们只需要判断:对于每一段,如果该段长度大于1且第一个字符是'0',或者该段长度等于1且该段为"0",那么就不合法?实际上,因为题目要求加数大于0,所以不允许0出现,因此只要一段的第一个字符是'0',那么这段就不合法(因为长度大于1时,有前导零;长度等于1时,该数为0,也不合法)。所以我们可以统一为:只要该段第一个字符是'0',就不合法。 但是,有一种情况:一段是"0x"(x是任意数字),那么第一个字符是0,所以非法。而一段是"00",也是非法。一段是"0"(长度1),也是非法。 因此,我们只需要在枚举分割方案时,对每一段检查:如果该段的第一个字符是'0',那么跳过该方案。 具体步骤: 1. 方案数:用位运算枚举所有间隔的选择情况(0表示不插入,1表示插入)。总共有2^(n-1)种情况。 2. 对于每一种枚举,我们将字符串按照1的位置进行分割,然后检查每一段是否合法(即每段的第一个字符不能是'0')。 3. 如果合法,则计算该表达式的(将每一段转换成数字相加)。 4. 记录方案数,并更新最大值、最小值,以及累加总。 但是,注意:n最大为18,那么间隔有17个,2^17=131072,这是可以接受的。 但是,计算总的时候,如果每次都将字符串转换成数字,转换操作的时间复杂度为O(n),那么总时间复杂度为O(2^(n-1)*n),最大为131072*18≈2.36e6,可以接受。 但是,我们也可以预处理区间数字值,避免重复转换。我们可以用dp[i][j]表示从i到j(包括ij)形成的数字。但是这里n最大18,预处理也可以,但枚举分割时还是要一段段取,不如直接转换。 不过,为了避免重复转换,我们可以用记忆化:在枚举分割时,我们按分割点取子串,然后转换成数字。由于子串很多重复,但总子串数量是O(n^2)的,所以我们可以提前预处理所有子串对应的数字。但是n只有18,子串数量大约18*18/2=162,所以预处理也可以。 但是,为了简单,我们可以直接转换,因为2^17*18的转换次数并不大。 另一种思路:动态规划dp[state]?但状态不好表示。我们要求的是总,最大值,最小值,方案数,所以我们可以用四个数组: dp_count[i]:表示前i个字符构成的合法方案数。 dp_max[i]:前i个字符构成的所有合法表达式的的最大值。 dp_min[i]:前i个字符构成的所有合法表达式的的最小值。 dp_sum[i]:前i个字符构成的所有合法表达式的总。 但是,注意:表达式是求,所以整个表达式是多个加数的。那么,我们考虑最后一个加数,它可以是[j...i](j从1到i)这一段。那么转移: 如果[j...i]这一段是合法的(即s[j]!='0',或者如果j==i且s[j]=='0'?但0不允许,所以要求s[j]!='0'),那么: dp_count[i] += dp_count[j-1] (注意j从1开始,那么j-1就是上一段结束的位置,注意字符串下标从0开始) 但是,我们这样定义:字符串下标0..i-1(共i个字符),那么我们可以定义dp[0]=1(空串有一种方案,但注意空串后面没有数字,所以实际上我们dp[i]表示前i个字符的合法方案数,那么dp[0]=1作为边界,然后从1开始到n)。 转移: for j in range(0, i): # j是当前段的起始位置?或者结束位置?我们考虑当前段是[k, i-1] 令k从0到i-1,如果从k到i-1这一段是合法的,那么: dp_count[i] += dp_count[k] (但是注意,这里k表示前k个字符已经处理完了,那么当前段就是k到i-1,所以要求k<=i-1) 但是,这样定义的话,dp_count[k]表示前k个字符的合法方案数,然后当前段是[k, i-1](长度为i-k),那么总方案数就是dp_count[k] * 1(因为当前段只有一种情况,但注意,当前段本身是确定的,所以不需要乘方案数,只是将状态从k转移到i)。 同时,对于表达式的总,我们有: 设num = stoll(substr(k, i-k)) // 从k到i-1的子串对应的数字 那么,dp_sum[i] += dp_sum[k] + dp_count[k] * num 解释:对于前k个字符的每一种方案,整个表达式的都需要加上num(因为当前段作为一个加数)。所以,总 = 所有k的情况下(dp_sum[k] + dp_count[k]*num)之。 但是,注意:如果k=0(即当前段是整个字符串的第一段),那么dp_sum[0]是0,dp_count[0]是1(空串),那么表达式=0+1*num=num,正确。 对于最大值最小值: dp_max[i] = max{ dp_max[k] + num } ? 不对,因为dp_max[k]表示前k个字符的最大,然后加上当前段的数字num,所以是dp_max[k]+num。但是,这里要求k是从0到i-1且当前段合法。 同理,dp_min[i] = min{ dp_min[k] + num } 但是,注意:当k=0时,dp_max[0]应该是多少?因为前面没有数字,所以dp_max[0]应该是0,然后当前段是num,所以dp_max[i]至少是num。但是,如果整个字符串只有一段,那么表达式就是num。 因此,我们定义: dp_count[0] = 1 dp_sum[0] = 0 dp_max[0] = 0 // 注意,这里dp_max[0]不能设为负无穷,因为后面要加num,所以设为0。但是当i=0时,表达式应该是0?但实际表达式至少有一个数,所以当i>0时,我们不会使用dp_max[0]来更新最大值(因为后面有num,而num>0)?不对,当整个字符串作为一段时,k=0,那么dp_max[0]+num=0+num=num,这是正确的。 但是,我们要求表达式最小值,如果dp_min[0]设为0,那么dp_min[i]=min{ dp_min[k]+num },当k=0时,就是0+num=num。但是,如果整个字符串有多段,那么dp_min[k]可能更小?不对,因为dp_min[k]表示前k个字符的最小,然后加上当前段的num。 所以,动态规划的状态: 从0到n(n为字符串长度),dp_count[0]=1, dp_sum[0]=0, dp_max[0]=0, dp_min[0]=0? 但是注意,dp_min[0]如果设为0,那么当我们取第一段时,表达式就是num,这是正确的。但是,如果我们设dp_min[0]为0,那么当后面有多个段时,dp_min[k](k>0)可能比0大,那么取min时,0+num可能不是最小值。但是,实际上,dp_min[k](k>0)表示前k个字符的最小,这个至少为一个正数(因为每个加数>0),所以dp_min[k]至少大于0。因此,我们更新dp_min[i]时,考虑所有k,其中k=0时,dp_min[0]=0,所以0+num就是num,然后其他k(k>0)时,dp_min[k]>=某个正数,然后加上num,所以一定大于num?不对,因为dp_min[k]可能比num小吗?不一定,因为前面可能有多个加数,但是每个加数都是正数,所以dp_min[k]至少等于k个数字的最小,而num可能是一个很大的数(比如当前段很长)。所以,我们需要比较所有k对应的值。 但是,有一个问题dp_min[0]为0,那么当我们取第一段时,表达式就是num(比如整个字符串作为一段,那么num就是整个数字,可能很大)。而如果我们不取整个字符串作为一段,而是分成多段,那么dp_min[k](k>0)可能更小。比如"12",分成1+2,那么dp_min[2] = min{ dp_min[0]+12, dp_min[1]+2 },其中dp_min[1]表示前1个字符的最小,即1(因为前1个字符只有一段:1),然后加上当前段2,得到3;而dp_min[0]+12=12。所以取min(12, 3)=3。正确。 因此,动态规划方程: 令s为输入字符串,下标0~n-1。 我们定义: dp_count[0] = 1 dp_sum[0] = 0 dp_max[0] = 0 // 注意,这里dp_max[0]不能是-INF,因为后面加正数,但考虑到最大值,如果设为0,那么dp_max[i]至少是num(当前段),这是正确的。 dp_min[0] = 0 // 这里dp_min[0]设为0,因为后面加正数,所以dp_min[i]至少为num(当前段),然后其他分割方式比较。 但是,dp_min[0]设为0,那么当i>0时,dp_min[i]会取到从0转移过来的值(即整个字符串作为一段)其他分割方式的最小值。正确。 但是,注意:dp_min[0]设为0,那么dp_min[0]表示空串的表达式为0?实际上,空串的表达式应该是0,但题目要求表达式至少有一个数,所以当我们加入第一段时,表达式就是num,所以0作为起点是合理的。 然后,对于i从1到n(dp数组的下标i表示前i个字符,即子串s[0:i]): dp_count[i] = 0 dp_max[i] = 一个很小的数(比如-1e18) dp_min[i] = 一个很大的数(比如1e18) dp_sum[i] = 0 然后,枚举最后一段的起点j(j从0到i-1),最后一段就是s[j:i](子串从j到i-1,长度为i-j)。注意:最后一段不能有前导零,所以如果s[j]=='0'并且i-j>1,则跳过(注意:如果最后一段长度为1且s[j]=='0',那么0也不允许,所以只要s[j]=='0',那么最后一段就不合法,跳过)。 如果合法,则计算num = stoll(s.substr(j, i-j)) // 注意:这里n最大18,所以不会溢出(long long可以表示18位数)。 然后: dp_count[i] += dp_count[j] dp_sum[i] += dp_sum[j] + dp_count[j] * num // 因为每种方案都要加上num,所以总共加 dp_count[j] * num dp_max[i] = max(dp_max[i], dp_max[j] + num) dp_min[i] = min(dp_min[i], dp_min[j] + num) 注意:这里j从0到i-1,当j=0时,最后一段就是整个字符串(前i个字符),此时dp_count[0]=1,dp_sum[0]=0,所以dp_count[i]加1,dp_sum[i]加0+1*num=num,dp_max[i]dp_min[i]分别更新为0+num=num。 但是,这样会漏掉什么吗?比如,我们枚举最后一段,那么前面的部分(0到j-1)已经是一个合法的表达式,然后加上最后一段。这确实覆盖了所有情况。 但是,注意:最后一段的起点j必须满足:前j个字符是已经处理过的,即dp_count[j]>0。如果dp_count[j]==0,说明前j个字符没有合法方案,那么也不能转移。 因此,我们要求dp_count[j]必须大于0。 但是,由于我们枚举j从0开始,且dp_count[0]=1,所以j=0总是可以转移的(只要最后一段合法)。 那么,整个字符串"0"的情况:如果字符串是"0",那么i=1,j=0,最后一段s[0:1]="0",此时因为s[0]=='0'且长度1,但题目不允许0,所以跳过。那么dp_count[1]就为0,没有合法方案。正确。 另外,如果字符串开头是0,比如"0123",那么当j=0时,最后一段为"0"(不合法),跳过;当j=1时,最后一段为"1"(合法),但前1个字符(即s[0:1]="0")的dp_count[1]是多少?因为前1个字符就是"0",所以dp_count[1]应该是0(因为"0"不合法),所以j=1时,dp_count[1]为0,不能转移。所以整个字符串"0123"没有合法方案?但是题目要求加数大于0,所以整个字符串"0123"作为表达式,实际上是0123,但整数123?不对,题目要求数字不能有前导零,所以0123是不合法的(因为0开头且长度大于1)。所以整个字符串不能作为合法表达式。所以确实没有合法方案。 因此,动态规划可以解决。 但是,注意:枚举最后一段时,j的范围是0到i-1,但注意:最后一段的起点j必须使得前j个字符是合法的(即dp_count[j]>0),否则无法转移。因此,我们只需要枚举j,并且当dp_count[j]>0且最后一段合法时,才进行转移。 但是,我们也可以不检查dp_count[j]>0,因为如果dp_count[j]==0,那么加上去也没用(加0)。所以可以不检查,直接枚举。 但是,我们初始化dp_count[0]=1,其他dp_count[i]初始为0,然后从i=1开始更新。 最后,答案就是: 方案数:dp_count[n] 最大表达式dp_max[n] 最小表达式dp_min[n] 表达式总dp_sum[n] 但是,注意:样例1:s="1023", n=4 我们需要计算dp[4]。 转移时,枚举最后一段的起点j:j=0,1,2,3 j=0: 最后一段为"1023",因为s[0]=='1',所以合法。num=1023。 dp_count[4] += dp_count[0] = 1 dp_sum[4] += dp_sum[0] + dp_count[0]*1023 = 0+1*1023=1023 dp_max[4] = max(初始值, dp_max[0]+1023) = 0+1023=1023 dp_min[4] = min(初始值, 0+1023)=1023 j=1: 最后一段为s[1:4]="023",但是s[1]=='0'且长度3>1,不合法,跳过。 j=2: 最后一段为s[2:4]="23",合法,num=23。然后需要dp_count[2](前2个字符的合法方案数)是多少? 前2个字符"10":有两种分割方式吗?不,在dp[2]时: 计算dp[2]:枚举最后一段的起点k:k=0,1 k=0: 最后一段"10":合法,num=10 -> dp_count[2] += dp_count[0]=1, dp_sum[2]=10, dp_max[2]=10, dp_min[2]=10 k=1: 最后一段"0":不合法(因为s[1]=='0'且长度1,所以不允许0),跳过。 所以dp[2]={1,10,10,10} 因此,j=2:最后一段"23",那么: dp_count[4] += dp_count[2] = 1 -> 变为2 dp_sum[4] += dp_sum[2] + dp_count[2]*23 = 10 + 1*23 = 33 dp_max[4] = max(1023, 10+23)=1023 dp_min[4] = min(1023, 10+23)=33 j=3: 最后一段为s[3:4]="3",合法,num=3。需要dp_count[3](前3个字符的合法方案数)是多少? 计算dp[3]:枚举最后一段起点k:k=0,1,2 k=0: 最后一段"102" -> 合法?因为s[0]!='0',所以合法。num=102 -> dp_count[3] += dp_count[0]=1, dp_sum[3]=102, dp_max[3]=102, dp_min[3]=102 k=1: 最后一段"02" -> 不合法(s[1]=='0'且长度2>1),跳过。 k=2: 最后一段"2" -> 合法,num=2 -> 需要dp_count[2](前2个字符的方案数)=1,所以: dp_count[3] += dp_count[2] = 1 -> 总共2 dp_sum[3] += dp_sum[2] + dp_count[2]*2 = 10 + 1*2 = 12 dp_max[3] = max(102, 10+2)=102 dp_min[3] = min(102, 12)=12 所以dp[3]={2, 102+12? 不对,dp_sum[3]应该是102+12=114?不对,我们分别计算: k=0: 方案1:整个"102" -> 表达式=102 k=2: 方案2:前两个字符分成"10"(一段),然后加上"2" -> 表达式=10+2=12 dp_sum[3] = 102+12 = 114 dp_max[3]=102, dp_min[3]=12 然后j=3:最后一段"3": dp_count[4] += dp_count[3] = 2 -> 变为4 dp_sum[4] += dp_sum[3] + dp_count[3]*3 = 114 + 2*3 = 120 dp_max[4] = max(1023, 102+3, 12+3) -> 1023105,15比较,最大值还是1023 dp_min[4] = min(33, 102+3, 12+3)=min(33,105,15)=15 所以dp_sum[4]=1023+33+120=1176?不对,这里dp_sum[4]=1023(j=0) + 33(j=2) + 120(j=3)? 不对,我们每次j循环是累加: j=0: 1023 j=2: 33 -> 累计1023+33=1056 j=3: 120 -> 累计1056+120=1176 正确。 所以方案数=4,最大值=1023,最小值=15,总=1176。 但是,我们上面计算dp_sum[4]时,j=3的贡献是114+2*3=120,而dp_sum[3]是114(前3个字符的表达式的总),然后加上最后一段3,并且有两种方案(前3个字符有两种方案),所以每种方案都要加上3,所以是2*3,然后再加上前3个字符的表达式总114(因为每种方案都要加上3,所以相当于在原来的表达式上加上3乘以方案数)。 这个动态规划是正确的。 但是,注意:我们枚举最后一段的起点j时,最后一段是s[j:i](注意:i表示前i个字符,所以最后一个字符下标是i-1)。所以子串为s[j]到s[i-1]。 另外,我们使用long long,因为数字最大18位,所以不会溢出,但是总可能会很大?比如最多2^(n-1)种方案,n最大18,即2^17=131072种方案,每个表达式最大为10^18,那么总最大131072*10^18 ≈ 1.3e23,long long最大为9e18,所以会溢出。因此,我们需要使用更大的整数类型,比如unsigned long long(最大1e19)也不够,所以要用__int128(如果编译器支持)或者用高精度。但是题目数据范围:n最大18,但测试点9~10是10^18(这里的10^18是指数字的大小,但字符串长度最多18,所以分割方案最多2^17=131072种,表达式最大就是整个数字(10^18),那么总最大131072*10^18=1.3e23,这超过了long long(9e18)unsigned long long(1.8e19),所以我们需要用__int128(可以表示约3.4e38)或者用高精度。 但是,题目要求输出三个整数,但样例输出中,12345678的总是17577216(大约1e7),所以实际数据可能不会达到1e23。但是,我们看样例输入3:12345678,输出总17577216,这个值很小。为什么?因为分割后,每个加数都很小,所以总不会很大。但是,我们考虑最坏情况:整个数字非常大(比如10^18)且只有一种方案(整个字符串作为一段),那么总就是10^18。而方案数最多131072种,那么所有方案总最大就是131072 * 10^18 = 1.3e23,这个数超过了long long。 但是,题目数据范围中,测试点5~6是n≤100000,但是这里n是数字的位数?题目描述输入是一个整数n,但后面说“王老师拥有的数字”,所以输入是一个整数,可能很大,所以用字符串处理。题目数据范围表格中“n≤”指的是数字的位数?表格中测试点1:10,测试点2:100,测试点3~4:1000,测试点5~6:100000,测试点7~8:10,测试点9~10:10^18。这里表格中n≤100000,指的是数字的位数?100000位?但是2^(99999)是天文数字,枚举不可能。所以这里表格中的n应该是数字的值?不对,输入是一个数字,这个数字最大可以达到10^18(即18位),所以位数最多18位。 再看表格:测试点1:n≤10,这里n应该是数字的值(整数)?但是10是一位数,而测试点2:n≤100(两位数或三位数?100是三位数),测试点3~4:1000(四位数),测试点5~6:100000(六位数),测试点7~8:10(一位数或两位数?),测试点9~10:10^18(最多18位数)。 因此,实际上,数字的位数最多18位。所以我们的动态规划是O(n^2)的(因为i从1到18,j从0到i-1,总共18*18=324次循环),所以完全可以用O(n^2)的动态规划。 但是,为什么测试点5~6是100000?这里100000应该指的是数字的值不超过100000(即最多5位数),所以位数最多5位,那么动态规划只需要O(5^2)=25次循环。 所以,我们按照字符串长度来,长度最大18,所以O(n^2)的循环次数最多324次,完全可以。 但是,总可能会溢出long long吗?最大方案数:2^(len-1),len最大18,最大方案数2^17=131072。而每个表达式的最大为10^18,那么总最大131072 * 10^18 = 1.3e23,这个数超过了unsigned long long(1.8e19)long long(9e18)。所以我们需要用__int128来存储dp_sum。但是,题目输出要求是整数,而样例输出都是整数,且最小总15,最大1023,样例3总17577216,所以实际数据可能不会达到1e23。但是,我们为了保证正确性,还是用__int128。不过,注意:如果评测机不支持__int128,那么我们可以用高精度?但是题目数据范围中,总最大1.3e23,这个数有80位二进制,所以高精度比较麻烦。或者我们分析一下:最坏情况,数字是999...9(18个9),那么整个表达式就是10^18-1,方案数最多131072,那么总最大(10^18-1)*131072≈1.31e23,这个数可以用double近似表示?但题目要求精确整数。 但是,我们看样例3:12345678,方案数128,总17577216,这个数很小。为什么?因为分割后,每个加数都很小。实际上,最坏情况是每个分割点都插入加号,那么表达式=1+2+3+...+9(如果数字是123456789)?不对,数字是给定的,比如一个所有位都是1的18位数,那么总=方案数*(1+1+...+1)?不对,因为每个方案的不同。 但是,动态规划中,我们计算dp_sum[i]的公式:dp_sum[i] = sum_{j} (dp_sum[j] + dp_count[j]*num) 这个公式中,dp_count[j]dp_sum[j]在j递增时,会指数级增长(方案数是指数增长的),而num最大为10^18,所以总可能会很大。 因此,为了避免溢出,使用__int128。如果编译器不支持__int128,那么我们可以用long double来存?不行,因为要精确整数。或者用高精度。 但是,题目没有说明数据范围,但根据提示,数字位数最多18位,方案数最多131072,而表达式最大为整个数字(10^18),所以总最大131072*10^18=1.31e23,这个数可以用字符串表示,但输出时,题目要求输出整数,所以如果超过long long,我们需要写高精度输出。但是,题目样例输出都是普通整数,所以实际数据可能不会达到那么大。 我们再看样例输入2:1005,输出总1110。 1005有两种方案:10051+005(但005不合法,所以只有1005100+5?不对,样例输出2:2 1005 105 1110,所以两种方案:1005(=1005)100+5(=105),所以总=1005+105=1110。 所以,总就是所有方案的相加。 因此,我们使用__int128来存储dp_sum,dp_max,dp_min。但是dp_maxdp_min最大10^18,所以用long long也可以(因为10^18<2^60,而long long最大2^63-1,所以可以)。但是dp_sum可能很大,所以dp_sum用__int128。 但是,输出时,如果评测机不支持__int128,那么我们可以用字符串输出__int128?或者我们判断如果支持__int128就用,否则用高精度?这里我们假设评测机支持__int128(现在大多数OJ都支持)。 如果不支持,我们也可以用两个long long来模拟,或者用现成的高精度库,但题目要求只能用C++,而且代码不能有注释,所以我们可以自己写一个简单的__int128输出函数。 由于题目输出最后三个数,而dp_maxdp_min我们用long long存储(因为最大值最小值不会超过整个数字,而整个数字最大10^18,所以long long足够),dp_sum用__int128。 但是,动态规划中,dp_maxdp_min在转移时,需要加上num(long long),而dp_max[i]dp_min[i]我们定义为long long。 因此,我们定义: vector<long long> dp_count(n+1, 0); // 方案数,最多131072,long long可以 vector<__int128> dp_sum(n+1, 0); // 总,可能很大 vector<long long> dp_max(n+1, 0); // 最大值,最大10^18,所以用long long vector<long long> dp_min(n+1, 0); // 最小值 但是,dp_maxdp_min的初始值: dp_max[0]=0, dp_min[0]=0 dp_count[0]=1 dp_sum[0]=0 然后,对于i>=1,初始化dp_max[i]为一个很小的值,dp_min[i]为一个很大的值。但是,dp_max[i]dp_min[i]是long long,所以: dp_max[i] = LLONG_MIN; // 不行,因为转移时可能加上一个正数,然后比LLONG_MIN大,但是LLONG_MIN是一个很小的负数,如果我们没有转移成功,那么最后dp_count[i]可能为0,那么我们就不能输出这些值(题目要求输出三个整数,但如果没有合法方案,那么最大值最小值怎么定义?题目没说明,但样例至少有一组方案(整个字符串))。所以题目保证至少有一种方案(整个字符串,而且字符串是正整数,所以整个字符串是合法的,因为题目说输入是正整数)。所以我们可以不考虑无方案的情况。 但是,我们初始化dp_count[i]=0,如果最后dp_count[i]为0,那么我们就跳过。但是题目保证有方案(整个字符串),所以最后dp_count[n]>0。 因此,我们初始化: dp_max[0]=0, dp_min[0]=0 for i from 1 to n: dp_max[i] = LLONG_MIN; // 或者我们初始化为一个很小的数,但为了安全,初始化为一个不可能的值,然后如果没有转移就保留,但我们知道一定有整个字符串这一种方案(j=0),所以一定会被更新,所以可以初始化为LLONG_MINLLONG_MAX。 但是,j=0时,最后一段就是整个字符串,只要整个字符串合法(即第一个字符不是0,或者长度为1且不是0?但题目要求正整数,所以整个字符串一定合法?因为输入是正整数,所以字符串第一个字符不是0(除非是0,但题目数据范围最小为1),所以j=0一定合法?不一定,如果字符串是"0",那么不合法。但是题目数据范围最小为1,所以输入字符串第一个字符不是0。因此,j=0一定合法。 所以,我们可以在循环j=0时,直接赋值,然后j>0时再更新。 但是,为了代码统一,我们按动态规划的方式。 代码步骤: 1. 读入字符串s,长度len。 2. 定义n=len。 3. 初始化四个数组:dp_count, dp_sum, dp_max, dp_min,长度为n+1。 4. dp_count[0]=1, dp_sum[0
最新发布
11-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值