紫书第八章竞赛题目
作者:互联网
抄书(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