其他分享
首页 > 其他分享> > 紫书第八章竞赛题目

紫书第八章竞赛题目

作者:互联网

抄书(Copying Books, UVa 714)

题目链接

输入输出

第一行代表有n组数据,以下n组数据每组两行,第一行是m和k,第二行的m个数是序列。

对于每组输入,输出最优的划分方式。

Sample Input
2
9  3
100  200  300  400  500  600  700  800  900
5  4
100  100  100  100  100

Sample Output
100  200  300  400  500  /  600  700  /  800  900
100  /  100  /  100  /  100  100

思路

此题没法暴力破解,如果仅从这个序列上思考,很难思考出个可行的,在规定时间范围内能完成的解决方法。

紫书中是这样想的。

设定谓词\(P(x)\)代表的含义为,是否能在每组子序列的和都不超过x的前提下将原序列划分成k个子序列。若能分成,这个x可能就是答案中最大的\(S(i)\)或者比\(S(i)\)还大,那就使用二分查找搜索左边,如果不能分成,就证明答案中最大的\(S(i)\)一定比x大,使用二分查找搜索右边。

而\(P(x)\)的执行只需要一次遍历,时间复杂度是\(O(n)\),外层的二分查找是\(O(log_2n)\),那么总的复杂度就是\(O(nlog_2n)\)。

我试着对\(P(x)\)的时间复杂度来优化,可以经过一次预处理将序列的后n项和算出来,然后因为序列中都是正整数,这个后n项和肯定是递增的,所以这里也能够使用二分查找。但写着写着就写乱了,所以效果不是特别好,代码将就着看吧,看不懂的话不怪你,怪我写的太乱了。。我尽量多写些注释。

代码

#include "iostream"
#include "cstdio"
#include <algorithm>
#include <queue>
#include "cmath"

using namespace std;

#define MAX 500
#define Num long long

// 输入序列
int seq[MAX];
/*
 后n项和序列
 假设输入序列是   2 3 1 4 5
 后n项和序列则是  15 13 10 9 5
 */
Num S[MAX];
int m, k,max_n=0;
// deque用于暂存P(x)中选择的分割线插入位置,min_ans代表当前找到的最小x的分割线插入位置
deque<int> pos,min_ans;
bool cmp_desc(Num a, Num b) {
    return a > b;
}
bool P(Num x) {
    pos.clear(); 
    Num find = x,*it;
    // 尝试在每组数和<=x的情况下分割成k组
    for (int i = 0; i < k; i++) {
        // 如果只剩下最后一组数,那么只需要判断最后一组加起来是不是<=x,是的话就能分割
        if (i == k - 1) {
            if (S[0] - (*it) <= x)return true;
        }
        else {
            // 如果不是最后一组数,那么每次都在后n项和序列中找到我们当前要寻找的数,如果没有此数,返回比它要大的第一个,这个数就是分割的位置
			it = lower_bound(S, S + m, find,cmp_desc);
            // 如果找到头了,结束查找 其实这里逻辑有问题,因为第一个有可能直接大于x,不过不影响算法的正确性,这是我提交后才想到的
			if (it == S)return true;
            // 插入分割位置
            pos.push_front(it - S -1);
            // 迭代find
			find = x + (*it);
        }
    }
    return false;
}
void showAns() {
    int j = 0;
    for (int i = 0; i < m; i++) {
        cout << seq[i];
        if (i != m - 1)cout << " ";
        if (j<min_ans.size() && min_ans[j] == i) { cout << "/ "; j++; }
    }
    cout << endl;
}
int main() {
    int kases;
    scanf("%d", &kases);
    for (int c = 0; c < kases; c++) {
        max_n = 0; min_ans.clear(); pos.clear();
        scanf("%d %d", &m, &k);
        for (int i = 0; i < m; i++) {
            scanf("%d", &seq[i]);
        }
        // 如果k为1,不用分割
        if (k == 1) {
            showAns(); continue;
        }
        // 计算后n项和
        for (int i = m - 1; i >= 0; i--) {
            if (i == m-1)S[i] = seq[i];
            else S[i] = seq[i] + S[i + 1];
            max_n = max(max_n, seq[i]);
        }

        // 初始化左边界和右边界,开始二分查找
        // 对于P(x),x不可能比整个序列中的最大值还要小
        // 也不可能比整个序列的和还要大
        Num l = max_n, r = S[0],min_mid=r;
        while (l < r) {
            Num mid = (l + r) / 2;
            if (P(mid)) {
                if (mid < min_mid) {
                    min_mid = mid;
                    // 因为P函数中只是对能不能划分做了一个测试,有的时候返回的pos是缺少或不准确的,所以如果pos不准确,就重新计算。
                    if (pos.size() == k - 1) {
                        min_ans = pos;
                    }
                    else {
                        // 重新计算答案
						min_ans = deque<int>();
						int s=0;
						for (int i = m-1; i >= 0; i--) {
							s += seq[i];
                            // 因为mid传入了P谓词,所以mid就是上面所说的x。
                            // 若当前加和已经大于x了,或者前面的数字个数最多只能再分出剩下的组数时
                            // 插入分割线
							if (s>mid || k - min_ans.size() == i+2) {
								min_ans.push_front(i);
								s = seq[i];
							}else if(s==mid){
                                // 若当前加和等于x,在前面插入分割线
								min_ans.push_front(i-1);
								s = 0;
							}
                            // 分组完毕,分割线数==组数-1
							if (min_ans.size() == k - 1) { break; }
						}
                    }
                }
                r = mid;
            }
            else {
                l = mid + 1;
            }
        }
		showAns();
    }
    return 0;
}

标签:题目,紫书,min,mid,第八章,Num,ans,序列,100
来源: https://www.cnblogs.com/lilpig/p/14297277.html