信息学奥赛一本通 1278:【例9.22】复制书稿(book) | 洛谷 P1281 书的复制

【题目链接】

ybt 1278:【例9.22】复制书稿(book)
洛谷 P1281 书的复制

【题目考点】

1. 动态规划:线性动规

【解题思路】

该题可以抽象为:将由m个数字构成的序列分成k个子段。对于每种子段分割方案,都能求出所有子段中的最大子段和。求所有方案中最大子段和最小的分割子段的方案。

解法1:二分答案

信息学奥赛一本通 1243:月度开销 | OpenJudge NOI 1.11 06:月度开销
本题与上题解决的是同一个问题。

1. 确定二分答案条件

记要确定的最大子段和为x,求最少子段数量的方法:顺序遍历数组,对于每个数字,只要能加入前一个子段后,子段和不超过x,那么就加入前一个子段。否则自己作为下一个子段的第一个元素。(上一题题解中有该贪心方法正确性的证明)。
在最大子段和为x的前提下,将原序列分割成的最少子段的数量如果小于等于k,那么再随意分割一下就可以让子段的数量等于k,因此该最大子段和x就是合理的。
由此得到二分查找的条件为:在最大子段和为x的前提下,将原序列分割成最少子段的数量小于等于k。求满足该条件的最大子段和x的最小值。

2. 输出子段

求出最大子段和x后,由于最终结果要让前面的人少抄写,也就是前面的子段尽可能短。那么可以从后向前遍历,如果当前数字可以加入当前子段(子段和小于等于x),那么加入;否则新开一个子段。这样做后面的子段就会尽可能长,前面的子段会尽可能短。最后用递归的方式倒序输出。

3. 复杂度

二分查找的过程复杂度为 O ( l o g ( s u m ) ) O(log(sum)) O(log(sum))所有数字加和sum可以认为小于 1 0 9 10^9 109 l o g 2 1 0 9 = 9 ∗ l o g 2 10 ≤ 9 ∗ l o g 2 16 = 36 log_210^9 =9*log_210\le9*log_216=36 log2109=9log2109log216=36,可以忽略不计。
后面输出子段的过程复杂度为 O ( n ) O(n) O(n),因此该方法复杂度为 O ( n ) O(n) O(n)

解法2:动态规划

1. 状态定义

集合:子段分割方案
限制:看前几个数字,分成几个子段
属性:最大子段和
条件:最小
统计量:最大子段和
状态定义dp[i][j]:将前i个数字分成j个子段的方案中最大子段和最小的方案的最大子段和。
初始状态dp[i][1]:前i个数字分成1段,就是前i个数字的和

2. 状态转移方程

记数组a的前缀和数组为s,a[i]~a[j]的子段和为s[j]-s[i-1]
集合:将前i个数字分成j个子段的方案
分割集合:根据第j子段的元素个数来分割集合

  • 如果第j子段只有a[i],那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前i-1个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段比较得到的最大子段和。dp[i][j] = max(dp[i-1][j-1], a[i])
  • 如果第j子段有a[i-1]a[i],那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前i-2个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段比较得到的最大子段和。dp[i][j] = max(dp[i-2][j-1], a[i]+a[i-1])
  • 如果第j子段为a[j]~a[i]。那么将前i个数字分成j个子段的最大子段和最小的方案的最大子段和,为将前j-1个数字分成j-1个子段的最大子段和最小的方案的最大子段和,再与第j子段的子段和s[i]-s[j-1]比较得到的最大子段和。dp[i][j] = max(dp[j-1][j-1], s[i]-s[j-1])
  • 综上,h从j开始增加到i(h最小时前h-1个数字与子段数j-1相等,有 h = j h=j h=j),取第j子段为a[h]~a[i],有dp[i][j] = max(dp[h-1][j-1], s[i]-s[h-1]))
  • 以上所有情况取最小值
3. 输出子段

和解法1中的方法一样
从后向前遍历,如果当前数字可以加入当前子段(子段和小于等于x),那么加入;否则新开一个子段。这样做后面的子段就会尽可能长,前面的子段会尽可能短。最后用递归的方式倒序输出。

4. 复杂度

外层i与j都是从1到500,内层h从j到i。

ij内层循环次数:i-j+1
111
122
1500500

i为1时,总循环次数为 ∑ h = 1 500 h \sum_{h=1}^{500}h h=1500h

ij内层循环次数:i-j+1
221
232
2500499

i为2时,总循环次数为 ∑ h = 1 499 h \sum_{h=1}^{499}h h=1499h
i从1到500,总循环次数为 ∑ i = 1 500 ∑ h = 1 500 − i + 1 h \sum_{i=1}^{500}\sum_{h=1}^{500-i+1}h i=1500h=1500i+1h,已知 ∑ i = 1 n = 1 2 ( 1 + n ) n \sum_{i=1}^n=\frac{1}{2}(1+n)n i=1n=21(1+n)n,则原式可以化简为: 1 2 ∑ i = 1 500 ( 502 − i ) ( 501 − i ) = 1 2 ∑ i = 1 500 ( i 2 − 1003 i + 251502 ) = 1 2 ( 1 3 i 3 − 1003 2 i 2 + 251502 i ) ∣ 1 500 = 1 2 ( 1 3 50 0 3 − 1003 2 50 0 2 + 251502 ∗ 500 − 1 3 − 1003 2 + 251502 ) ≈ 2.11 ∗ 1 0 7 \frac{1}{2}\sum_{i=1}^{500}(502-i)(501-i) = \frac{1}{2}\sum_{i=1}^{500}(i^2-1003i+251502)=\frac{1}{2}(\frac{1}{3}i^3-\frac{1003}{2}i^2+251502i)|^{500}_1=\frac{1}{2}(\frac{1}{3}500^3-\frac{1003}{2}500^2+251502*500-\frac{1}{3}-\frac{1003}{2}+251502)\approx 2.11*10^7 21i=1500(502i)(501i)=21i=1500(i21003i+251502)=21(31i321003i2+251502i)1500=21(315003210035002+2515025003121003+251502)2.11107
该复杂度可以接受。

【题解代码】

解法1:二分答案
#include <bits/stdc++.h>
using namespace std;
#define N 505
int m, k, a[N];
bool check(int x)//在最大子段和为x的前提下,将原序列分割成最少子段的数量是否小于等于k
{
    int sum = 0, ct = 1;//s:每个子段的和 ct:子段数量 
    for(int i = 1; i <= m; ++i)
    {
        if(a[i] > x)//如果某个元素大于x,那么不满足前提,条件不满足 
            return false;
        if(sum + a[i] > x)
        {
            ct++;
            sum = a[i];
        }
        else
            sum += a[i];
    }
    return ct <= k;
}
void show(int i, int x)//输出数组a[1]~a[i]中子段和小于等于x的所有子段左右端点(子段长度从小到大) 
{
    if(i == 0)
        return;
    int j, sum = 0;
    for(j = i; j >= 1 && sum+a[j] <= x; --j)
        sum += a[j];
    show(j, x);//输出a[1]~a[j]中的子段 
    cout << j+1 << ' ' << i << endl;  
}
int main()
{
    int sum = 0;
    cin >> m >> k;
    for(int i = 1; i <= m; ++i)
    {
        cin >> a[i];
        sum += a[i];
    }
    int l = 0, r = sum, mid;
    while(l < r)
    {
        mid = (l+r)/2;
        if(check(mid))
            r = mid;
        else
            l = mid+1;
    }
    show(m, l);//此时l为最小的最大子段和。按要求输出a[1]~a[m]中的所有子段 
    return 0;
}
解法2:动态规划
#include <bits/stdc++.h>
using namespace std;
#define N 505
int m, k, a[N], s[N];//a:数字序列 s:前缀和
int dp[N][N];//dp[i][j]:将前i个数字分成j个子段的方案中最大子段和最小的方案的最大子段和。
void show(int i, int x)//输出数组a[1]~a[i]中子段和小于等于x的所有子段左右端点(子段长度从小到大) 
{
    if(i == 0)
        return;
    int j, sum = 0;
    for(j = i; j >= 1 && sum+a[j] <= x; --j)
        sum += a[j];
    show(j, x);//输出a[1]~a[j]中的子段 
    cout << j+1 << ' ' << i << endl;  
}
int main()
{
    cin >> m >> k;
    for(int i = 1; i <= m; ++i)
    {
        cin >> a[i];
        s[i] = s[i-1] + a[i];
    }
    memset(dp, 0x3f, sizeof(dp));//dp初始化为无穷大 
    for(int i = 1; i <= m; ++i)//前i个数字分成1段,就是前i个数字的和
        dp[i][1] = s[i];
    for(int i = 1; i <= m; ++i)
        for(int j = 2; j <= k; ++j)
            for(int h = j; h <= i; ++h)//取子段a[h]~a[i] 
                dp[i][j] = min(dp[i][j], max(dp[h-1][j-1], s[i]-s[h-1]));
    show(m, dp[m][k]); 
    return 0;
}

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值