Loading

紫书第八章竞赛题目

抄书(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;
}

全部相加(Add All, UVa 10954)

有n(n≤5000)个数的集合S,每次可以从S中删除两个数,然后把它们的和放回集合, 直到剩下一个数。每次操作的开销等于删除的两个数之和,求最小总开销。所有数均小于\(10^5\)

输入输出

每组数据包含两行,第一行是一个整数n,第二行的n个数是输入序列。

对于每组输入,输出把他们全部相加的最小开销。

Sample Input
3
1 2 3
4
1 2 3 4
0

Sample Output
9
19

思路

这就是Huffman编码树,每次选择两个最小的,相加,并且把结果放到集合中。

关于算法的正确性,这是一个贪心算法,因为想要总开销最小,并且相加的结果还要放回集合中,那么很自然的就能想到每次选择两个最小的数相加。

诶试试用反证法?

假设\(a\)\(b\)是当前集合中最小的,它们相加之后,总开销增加了\(a+b\),现在假设这个开销并不是当前阶段的最小开销,那么必然存在其他的一对数,比如\(c\)\(d\),并且\(c+d<a+b\),可是\(a\)\(b\)是当前集合中最小的,不可能大于\(c+d\),所以这个开销就是当前阶段的最小开销。

代码

#include "iostream"
#include "cstdio"
#include "queue"

using namespace std;

int main() {
    int n;
    priority_queue<int,vector<int>,greater<int> > q;
    while (scanf("%d", &n) != EOF,n) {
        while(!q.empty())q.pop();
        for (int i = 0; i < n; i++) {
            int t; scanf("%d", &t);
            q.push(t);
        }
        int ans = 0;
        while (q.size() > 1) {
            int a = q.top(); q.pop();
            int b = q.top(); q.pop();
            ans += a + b;
            q.push(a + b);
        }
        printf("%d\n", ans);
    }
    return 0;
}

奇怪的气球膨胀(Erratic Expansion, UVa12627)

一开始有一个红气球,每经过一个小时,一个红气球分裂成三个红气球,一个蓝气球。就像下图。

给你三个数,A,B,K,求经过K小时,[A,B]之间的红气球数量。

输入输出

第一行是一个数T,不超过1000,代表示例总数,以下的T行每行是一个示例,包含三个数,分别是K,A和B,K在[0,30]中。

对于每组示例,输出经过K小时,[A,B]之间的红色气球数量。

Sample Input
3
0 1 1
3 1 8
3 3 7

Sample Output
Case 1: 1
Case 2: 27
Case 3: 14

思路

想了三个版本的递推。

第一个版本是\(cnt(r,K)\)代表K小时后,r行的红气球数,则答案为。

\[\sum_{i=A}^B cnt(i,K) \]

那么cnt函数如何计算?通过观察发现,每隔一小时不过是把当前的图案作为下一次图案的左上角,并复制了两份,作为左下和右上,而右下角全是蓝色。那么,对于\(cnt(r,K)\),如果r在图形的下半部分\((r>2^{k-1})\),那么其中的红色气球数为$cnt(r-\(2^{k-1}\),k-1)$,就是当k-1小时时对应的上半部分的行的红色气球数量,因为是从那里复制过来的,所以没问题。对于r在上半部分就更简单了,可以变成\(2\times cnt(r,k-1)\)

我们应该给这个递推式一个基本解,就是当\(k=0,r=1\)时,有1个红色气球。

\[cnt(r,K)= \left\{ \begin{array}{l} cnt(r-2^{k-1},k-1)\ when \ (r>2^{k-1}) \\ cnt(r,k-1)\ else \end{array} \right. \\ cnt(1,0) = 1 \]

这不是一个好的递推算法,需要\(k\times (B-A)\)次循环。是\(O(n^2)\)的复杂度。

第二个版本的递推,是按区域来递推,省去了一层循环,但由于A和B的差异可能很大(最大可差\(2^{30}-1\)),所以这个线性时间的复杂度也难以应付题目中1000毫秒的要求,这里不写了。

第三个版本是紫书的版本,利用了一个我之前从没注意到的点。

\(c(k)\)是k小时后总共的红气球数,不难发现\(c(k) = 3^k\),因为每次复制三个原来的图形。

\(f(K,B)\)是k小时后前B行的红气球数,那么\(f(K,B) - f(K,A-1)\)就是所求的区间内红气球的数量。

现在只需要找到\(f(K,B)\)的递推关系就好了。

还是分成两部分,当B在上半部分时\((B<=2^{k-1})\),红气球的数量为\(2\times f(K-1,B)\),因为左右两边是一样的,只需要递推求解一个乘二即可,当在下半部分时,则为\(2\times c(K-1) + f(K-1,B-2^{k-1})\),加法中前面一项是上面一半所有的红气球数,因为都占满了,所以直接用c函数就可以计算,省去了很多递推的麻烦。

规定当\(f(0,1)=1\)\(f(K,r)(K<0||B<=0) = 0\)

时间复杂度\(O(K)\),虽然也是线性,但K的取值范围是[0,30],大大降低运行时间。

代码

#include "iostream"
#include "cstdio"
#include "cmath"

using namespace std;

#define LL long long

LL c(int k) {
    return pow(3, k);
}

LL f(int K, int B) {
    if (K == 0 && B == 1)return 1;
    if (K <= 0 || B <= 0)return 0;
    int km1pwr = 1 << (K - 1);
    if (B < km1pwr)return 2 * f(K - 1, B);
    return 2 * c(K - 1) + f(K - 1, B - km1pwr);
}

int main() {
    int T, K, A, B;
    scanf("%d", &T);
    for (int i = 1; i <= T; i++) {
        scanf("%d %d %d", &K, &A, &B);
        printf("Case %d: %lld\n",i,f(K,B) - f(K,A-1));
    }
    return 0;
}
posted @ 2021-01-19 12:26  yudoge  阅读(86)  评论(0编辑  收藏  举报