书的复制(抄书问题)

题面简述

现在要把\(m\)本有顺序的书分给\(k\)给人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。

现在请你设计一种方案,使得复制时间最短。

输出共\(k\)行,每行两个整数,第\(i\)行表示第\(i\)个人抄写的书的起始编号和终止编号。\(k\)行的起始编号应该从小到大排列,复制时间为抄写页数最多的人用去的时间,如果有多解,则尽可能让前面的人少抄写。

解决问题

应同学的要求,我写了这篇题解。这道题目的做法好像有两种,动态规划法和二分答案法,我们先讲二分法。

二分答案法

二分答案的时候我们应该以每个人抄的书的本数作为二分标准,进行二分答案,如果分配的人数与题目要求的\(k\)不一样,则将二分区间缩小,如果相等,我们还需要进一步缩小上界,使得用时最短。

现在具体来讲一下二分答案的做法。首先确定上下界,因为每本书都会有人抄,那么我们下界就可以确定为书的页数的最大值(反证:如果二分的时候分到比这个最大值还小,那最大值的那本书就没人抄了),以减少二分次数;接下来确定上界,很明显,最坏情况就是只有一个人抄书,那么上界定为所有书页数之和。

确定好上下界之后,我们就可以进行二分了。最外层肯定是套标准的二分模板,但是二分中间该怎么处理呢?也不难,中间直接线性的扫一遍即可。需要注意的是,我们要使前面的人少抄,那么就得从后面开始分配任务,给后面的人分配尽可能多的任务(但不超过当前限定值)。当二分到恰好\(k\)个人能抄完时,别急着退出循环,我们应该继续缩小上界,求出最短时间。

具体实现如下:

#include <iostream>
using namespace std;
int main()
{
    int i, j, nmax = 0, flag = 0, m, k, l = 0, mid, h = 0, r[505] = {0}, man, co;
    int st[25][505], en[25][505];
    //丑得不堪入目QAQ
    cin >> m >> k;
    for(i = 1; i <= m; i++)
    {
        cin >> r[i];
        h += r[i];
        if(r[i] > l)
            l = r[i];//获取最大值
    }
    if(k == 1)
    {
        st[flag][1] = 1;
        en[flag][1] = m;
        //flag这一维很重要,因为按上面所说,恰好k个人之后还要找最小值
        //然而当上界缩小的时候就有可能改变人数,因此我们需要记录好之前的答案
        //第二维表示的是第几个人
    }
    while(l < h)
    {
        flag++;
        mid = (l + h) / 2;
        man = 0;
        i = m;//从后往前开始搜索
        while(i > 0)
        {
            en[flag][man+1] = i;
            //记住这是倒序分配的,人数也是倒的
            co = 0;//花费
            while(co <= mid && i >= 0)
                co += r[i--];
            i++;
            man++;
            st[flag][man] = i + 1;
            //因为是co<=mid,当其达到mid的时候它还是会再多抄一份,因此要减去
            if(man > k)
                break;
        }
        //二分模板
        if(man > k)
            l = mid + 1;
        else
        {
            nmax = flag;
            h = mid;
            //注意要求的尽量少的抄书数量
        }
    }
    for(i = k; i >= 1; i--)
        cout << st[nmax][i] << ' ' << en[nmax][i] << endl;
    return 0;
}

这是以前写的丑代码,请多多见谅。时间复杂度是\(O(m\lg\sum_{i=1}^kcost[i])\)的,用时0ms。但是坑爹的VsCode网站出了强化版且不说,它竟然还有抄书员不抄书的情况!我的程序目前过不了那个情况\(^*\),有时间再改改。

动态规划法

动态规划法似乎要慢一些,因为它相对于二分法来说,思维难度要简单,并且细节方面也不太坑。

现在我们详细讲讲动态规划的做法。这道题可以套上类似于钢条切割问题\(^1\)那样的做法,也就是先不考虑所有的书,就考虑前面几本书,将每种人数状态\(i\)\(1\leq i\leq k\))枚举出来,每种状态都找出一个最优解(可以通过前面的情况转移获得)。当书的数目这个状态增加之后,我们枚举每一个分割点,然后转移即可。写成递推式就是这样:

\[f[i][j]= \color{lightgreen}{\min}\color{s}{\{f[i][j]}, \color{lightgreen}{\max}\color{s}{\{f[i-1][s-1], sum[j] - sum[s-1]\}\}} \]

其中,\(f[i][j]\)表示\(i\)个人抄前\(j\)本书的最小耗费时间,\(sum[j]\)表示抄前\(j\)本书的时间,\(s\)代表当前枚举分割位置的状态。看过钢条切割的同学应该明白,新加的那个人直接抄完后面的所有书,而我们枚举了分割点\(s\),故不会遗漏,而里面取\(\max\)是因为要获得最大耗时,外面取\(\min\)是为了看看这个点之前算好了的状态和正在算的状态哪个更优,没计算之前赋值为\(+\infty\)

最后是打印的问题,学过钢条切割问题的同学们都知道,我们打印只能递归打印。打印的方法也不难,就是递归的时候,以答案(抄书的最小时间)为标准,从后往前扫描以及累加,打印累加值恰好小于等于答案的那个扫描位置,并在下层递归之后打印。这个有点抽象,但是你看到代码之后会恍然大悟的。

代码实现\(^2\)

#include <cstdio>
#include <algorithm>

using namespace std;

int sum[505], f[505][505], m, k;

void print(int x, int ans)
{
    if(!x)
        return;
    for(register int i = x; i >= 0; i -= 1)
        if(sum[x] - sum[i-1] > ans || !i)
        {
            print(i, ans);
            printf("%d %d\n", i+1, x);
            break;
        }
}

int main()
{
    scanf("%d%d", &m, &k);
    for(register int i = 1; i <= k; i += 1)
        for(register int j = 1; j <= m; j += 1)
            f[i][j] = 1e9;
    for(register int i = 1; i <= m; i += 1)
    {
        scanf("%d", &sum[i]);
        sum[i] += sum[i-1];
        f[1][i] = sum[i];
    }
    for(register int i = 2; i <= k; i += 1)
        for(register int j = i; j <= m; j += 1)
            for(register int s = 2; s <= j; s += 1)
                f[i][j] = min(f[i][j], max(f[i-1][s-1], sum[j] - sum[s-1]));
    print(m, f[k][m]);
    return 0;
}

时间复杂度为:\(O(km^2)\),不过由于数据挺小的,开个\(O2\)或者把取最大最小值函数改成自己写的差不多就能0ms了。

不过,动态规划还有没有可以优化的地步呢?答案是肯定的,但是时间上好像不能优化(博主本来想用四边形不等式优化,但是经过讨论之后发现有不止一个情况不满足条件)。

优化方案是:用滚动数组优化空间。因为涉及到的转移仅仅是\(f\)的左上位置,那么我们只需要倒序更新即可。

滚动数组优化部分:

for(register int i = 2; i <= k; i += 1)
    for(register int j = m; j >= i; j -= 1)
        for(register int s = 2; s <= j; s += 1)
            f[j] = min(f[j], max(f[s-1], sum[j] - sum[s-1]));

参考文献

  1. Thomas H.Cormen, Charles E.Leiserson, Ronald L.Rivest, Clifford Stein. 算法导论(第三版)[M]. 北京:机械工业出版社, 2012
  2. hongzy. 题解 书的复制. https://www.luogu.org/problemnew/solution/P1281, 2017-12-21

写在最后

感谢参考文献中提到的文献的帮助。
大多数内容为个人智力成果,如需转载,请注明出处,禁止作商业用途传播。
最后,感谢各位的阅读。

posted @ 2018-09-14 14:16  孤独·粲泽  阅读(1116)  评论(0编辑  收藏  举报