算法竞赛入门经典(第二版)-刘汝佳-第八章 高效算法设计 习题(18/28)

说明

本文是我对第8章28道习题的练习总结,建议配合紫书——《算法竞赛入门经典(第2版)》阅读本文。
另外为了方便做题,我在VOJ上开了一个contest,欢迎一起在上面做:第八章习题contest
如果想直接看某道题,请点开目录后点开相应的题目!!!

习题

习8-1 UVA 1149 装箱

题意
给定N(N≤10^5)个物品的重量Li,背包的容量M,同时要求每个背包最多装两个物品。求至少要多少个背包才能装下所有的物品。

思路
先对物品从小到大排序,然后从前往后贪心法搜索求解。初始化i=0, j=n-1,[i, j)区间表示未放入背包的物品,在(i, j)区间中搜索第一个大于M-a[i]的数k,则其左侧第一个数(也可能这个数就是第i个数)应该和第i个数放入同一个背包。[i, j)区间中,k和k后面的数应当放入单独的背包,因为剩下的物品中最轻的加上他们的各自重量都会超过容量M。

需要细心考虑多种情况。这里再提供两个测试用例:
INPUT

2
4 10
1 4 8 10
5 10
1 4 4 9 10

OUTPUT

3

3

代码

#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100001;

int n, l;
int a[N];

int main(void)
{
    int kase;
    scanf("%d", &kase);
    for (int t = 1; t <= kase; t++) {
        scanf("%d%d", &n, &l);
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        sort(a, a+n);

        int res = 0;
        int i = 0, j = n;
        while (i < j) {
            int k = upper_bound(a+i+1, a+j, l-a[i]) - a;
            if (k > i+1) k--;
            res += (j > k) ? j-k : 1;
            //printf("i=%d, j=%d, k=%d, res=%d\n", i, j, k, res);
            i++;
            j = k;
        }
        if (t > 1) printf("\n");
        printf("%d\n", res);
    }

    return 0;
}

习8-2 UVA 1610 聚会游戏

题意
输入一个n(2≤n≤1000,n是偶数)个字符串的集合D,找一个长度最短的字符串(不一定在D中出现)S,使得D中恰好一半串小于等于S,另一半串大于S。如果有多解,输出字典序最小的解。例如,对于{JOSEPHINE, JERRY},输出JF;对于{FRED, FREDDIE},输出FRED。提示:本题看似简单,实际上暗藏陷阱,需要考虑细致、周全。

思路
思路很简单,对字符串排序后找到最中间的两个字符串a和b,然后找到大于等于a且小于b的最短字符串中的字典序最小解。
但果然藏了非常多的陷阱,我一共WA了3发,最后一发还是粗心了,看了半天没看出来,参考了别人的博客才找到的错误。
这里提供一组测试数据吧,基本上能过这组数据的这个题应该就能AC了。
INPUT

2
F
EG
2
FH
EG
2
F
EZ
2
F
EZZE
2
F
EZZEFF
0

OUTPUT

EG
F
EZ
EZZE
EZZF

另外本题还有另一种思路,能够免除if else分析的麻烦。这样写的代码我估计一遍能够AC。
由于不能马上就直接得到答案,就一个一个字母去尝试。这样子就有点类似dfs了,假设a和b在第k位开始不同,先判断在这里填一个字母能否得出答案。这里需注意:不能填了一个字母后就立马搜索下一个情况,因为题目首先要求是最短,所以要在不填下一个字母的情况下,把这个位置可能的字母都填上试试。发现必须要再填下一个字母时,才开始填写下一个字母。
代码可参考这篇博文。

代码

#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1001;

int n;
string s[N];

int main(void)
{
    while (scanf("%d", &n) && n) {
        for (int i = 0; i < n; i++)
            cin >> s[i];
        sort(s, s+n);

        string a = s[n/2-1], b = s[n/2];
        int k;
        for (k = 0; k < min(a.size(), b.size()); k++)
            if (a[k] != b[k]) break;
        string res = a.substr(0, k);
        if (k < a.size()) {
            if (k+1 == a.size()) res += a[k];
            else if (k+1 == b.size() && b[k] - a[k] == 1) {
                res += a[k];
                for (int i = k+1; i < a.size(); i++) {
                    if(i == a.size()-1) {
                        res += a[i]; break;
                    } else if (a[i] != 'Z') {
                        res += (a[i]+1); break;
                    }
                    res += 'Z';
                }
            } else res += (char)(a[k]+1);
        }
        cout << res << endl;
    }

    return 0;
}

习8-3 UVA 12545 比特变换器

题意
输入两个等长(长度不超过100)的串S和T,其中S包含字符0, 1, ?,但T只包含0和1。你的任务是用尽量少的步数把S变成T。每步有3种操作:把S中的0变成1;把S中的“?”变成0或者1;交换S中任意两个字符。例如,01??00经过3步可以变成001010(方法是先把两个问号变成1和0,再交换两个字符)。

思路
贪心法求解。
由于只需要对不同的位置进行处理,先对两个字符串的不同位置进行分类统计,一共四种情况:

  1. S中为1,T中为0
  2. S中为0,T中为1
  3. S中为?,T中为0
  4. S中为?,T中为1

情况1不能由变换直接得到,需要与其他情况的位置交换得到,只能与情况2或4交换。与情况2交换只耗费1个操作,与情况4交换耗费2个操作(先将情况4的位置的?换成0,再交换)。如果情况1与其他情况交换后还剩余,则无解。情况1处理完后剩下的其他情况均只耗费1个操作。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

char s[101], t[101];

int main(void)
{
    int n;
    scanf("%d", &n);
    for (int k = 1; k <= n; k++) {
        scanf("%s%s", s, t);

        int type[4];
        memset(type, 0, sizeof(type));
        for (int i = 0; s[i]; i++) {
            if (s[i] != t[i]) {
                if (s[i] == '1') type[0]++;
                else if (s[i] == '0') type[1]++;
                else if (t[i] == '0') type[2]++;
                else type[3]++;
            }
        }

        int res = min(type[0], type[1]);
        type[0] -= res; type[1] -= res;
        if (type[0]) {
            int add = min(type[0], type[3]);
            type[0] -= add; type[3] -= add;
            if (type[0]) res = -1;
            else res += 2*add + type[3] + type[2];
        } else {
            res += type[1] + type[2] + type[3];
        }
        printf("Case %d: %d\n", k, res);
    }

    return 0;
}

习8-4 UVA 11491 奖品的价值

题意
你是一个电视节目的获奖嘉宾。主持人在黑板上写出一个n位整数(不以0开头),邀请你删除其中的d个数字,剩下的整数便是你所得到的奖品的价值。当然,你希望这个奖品价值尽量大。1≤d<n≤10^5。

思路
一开始真的没想到这竟然是一道贪心题目。看了别人的博客才恍然大悟。
我采取的做法是自前向后扫一遍,用vector存储选中的数,当前扫描的数s[i]大于vector尾部的数,那么从它开始将它及其它之前的比s[i]小的数全部删除。同时注意vector中数的个数加上剩下待扫描的数不能低于最终可选数n-d, 防止删除多了。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 100001;

int n, d;
char s[N];

int main(void)
{
    while (scanf("%d%d", &n, &d), n || d) {
        scanf("%s", s);

        vector<char> res;
        for (int i = 0; s[i]; i++) {
            int len;
            while (len = res.size()) {
                if (res[len-1] < s[i] && len+n-i > n-d)
                    res.resize(len-1);
                else
                    break;
            }
            res.push_back(s[i]);
        }
        for (int i = 0; i < n-d; i++)
            printf("%c", res[i]);
        printf("\n");
    }

    return 0;
}

习8-5 UVA 177 折痕

题意

思路

代码

习8-6 UVA 1611 起重机

题意
输入一个1~n(1≤n≤10000)的排列,用不超过96次操作把它变成升序。每次操作都可以选一个长度为偶数的连续区间,交换前一半和后一半。例如,输入5, 4, 6, 3, 2, 1,可以执行1, 2先变成4, 5, 6, 3, 2, 1,然后执行4, 5变成4, 5, 6, 2, 3, 1,然后执行5, 6变成4, 5, 6, 2, 1, 3,然后执行4, 5变成4, 5, 6, 1, 2, 3,最后执行操作1,6即可。
提示:2n次操作就足够了。

思路
顺序将1-n移到自己的位置上即可。将i移动到位置i时,先搜索i目前所在位置j,如果j超出了i与n的中间位置,说明一次操作不能到位,需要两次操作。所以总共最多需要2n次操作。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 10001;

int n;
int a[N];

typedef pair<int, int> P;
vector<P> res;

void exchange(int l, int r)
{
    res.push_back(P(l, r));
    int half = (r-l+1)/2;
    for (int i = l; i < l+half; i++)
        swap(a[i], a[i+half]);
    /* printf("=====exchange %d %d\n", l, r); for (int i = 1; i <= n; i++) printf("%d ", a[i]); printf("\n"); */
}

int main(void)
{
    int kase;
    scanf("%d", &kase);

    while (kase--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%d", &a[i]);

        res.clear();
        for (int i = 1; i <= n; i++) {
            int j;
            for (j = i; j <= n; j++)
                if (a[j] == i) break;
            if (i == j) continue;
            int half = j-i;
            if (2*half <= n-i+1) exchange(i, 2*half+i-1);
            else {
                half = (n-i+1)/2;
                exchange(n-2*half+1, n);
                exchange(i, 2*(j-half-i)+i-1);
            }
        }

        printf("%d\n", res.size());
        for (int i = 0; i < res.size(); i++)
            printf("%d %d\n", res[i].first, res[i].second);
    }

    return 0;
}

习8-7 UVA 11925 生成排列

题意
输入一个1~n(1≤n≤300)的排列,用不超过2n^2次操作将一个1-n的升序序列编程它。操作只有两种:交换前两个元素(操作1);把第一个元素移动到最后(操作2)。
书中说的是讲排列变成升序数列,事实上说反了。

思路
开始把问题想复杂了,自己考虑定义了一个广义的两个相邻数之间距离(这个距离对于操作2是不变的),如果交换能使得总距离变小则执行操作1,否则执行操作2。而当总距离达到最小且第一个数与排列的第一个数相等时结束循环。结果提交后TLE了,研究之后发现有一些情况下程序会进入死循环,无法下降到最小距离。

本题比较好的思路是逆向思考,把给定序列变成有序,操作相应变化一下,最后逆序输出操作。
至于排序的问题,把序列看成一个环,第二种操作相当改变了可交换元素的位置,然后就可以等效为冒泡排序啦。。。
但需要注意的一点是,是因为是环状的,和冒泡排序有所区别,最大的元素在头部的时候不能进行交换了,否则陷入死循环,最大的元素所在的位置相当与链状时候的最后面的有序区,是不需要比较的。

不过我本来是想通过逆序数来判断循环结束的,后来发现无法正确定义逆序数,因为这个排列是循环的。只好用比较土的方法——比较整个排列与结果相等——判断循环结束。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <deque>
#include <vector>
using namespace std;

int n;
deque<int> dq;

void print_dq()
{
    for (int i = 0; i < n; i++)
        printf("%d ", dq[i]);
    printf("\n");
}

int main (void)
{
    while (scanf("%d", &n) && n)
    {
        dq.clear();
        int tmp;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &tmp);
            dq.push_back(tmp);
        }

        vector<int> res;
        while (true) {
            bool flag = true;
            for (int i = 0; i < n; i++) {
                if (dq[i] != i+1) {flag = false; break;}
            }
            if (flag == true) break;
            if (dq[0] < dq[1] || (dq[0] == n && dq[1] == 1)) {
                res.push_back(2);
                dq.push_front(dq.back()); dq.pop_back();
            } else {
                res.push_back(1);
                swap(dq[0], dq[1]);
            }
            //print_dq();
        }
        for (int i = res.size()-1; i >= 0; i--)
            printf("%d", res[i]);
        printf("\n");

    }
    return 0;
}

习8-8 UVA 1612 猜名次

题意
有n(n≤16384)位选手参加编程比赛。比赛有3道题目,每个选手的每道题目都有一个评测之前的预得分(这个分数和选手提交程序的时间相关,提交得越早,预得分越大)。接下来是系统测试。如果某道题目未通过测试,则该题的实际得分为0分,否则得分等于预得分。得分相同的选手,ID小的排在前面。
问是否能给出所有3n个得分以及最后的实际名次。如果可能,输出最后一名的最高可能得分。每个预得分均为小于1000的非负整数,最多保留两位小数。

思路
贪心法求解。
要最高分,那第一名肯定要三道题都对。维护一个最高分和上一个人的ID号,接着判断一下下一名的得分。
如果有得分相同的情况下,就判断一下ID号。如果当前这个人的ID号比较大,就只需要更新ID就可以了。
如果没有得分相同的或者得分相同ID号比上一个人小,就找得分最大的且小于上一个人的得分的值即可。
如果上面两个条件都不满足,就是无解了。
另外需要注意判断小数相等不要直接用==号,要考虑浮点误差。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

const int N = 16385;
const double EPS = 1e-5;

int n;
double a[N][3];
int id[N];

int main(void)
{
    int kase = 0;
    while (scanf("%d", &n) && n) {
        for (int i = 1; i <= n; i++)
            scanf("%lf%lf%lf", &a[i][0], &a[i][1], &a[i][2]);
        for (int i = 1; i <= n; i++)
            scanf("%d", &id[i]);

        int mi;
        double mscore, pos[8];
        int i;
        for (i = 1; i <= n; i++) {
            int j = id[i];
            if (i == 1) {
                mi = i;
                mscore = a[j][0]+a[j][1]+a[j][2];
                continue;
            }
            int k;
            for (k = 0; k < 8; k++)
                pos[k] = (k&1)*a[j][0] + ((k>>1)&1)*a[j][1] + ((k>>2)&1)*a[j][2];
            sort(pos, pos+8);
            for (k = 7; k >= 0; k--) {
                if (pos[k] < mscore-EPS || j > id[i-1] && fabs(pos[k]-mscore) < EPS)
                    break;
            }
            if (k < 0) break;
            mscore = pos[k];
        }
        printf("Case %d: ", ++kase);
        if (i <= n) printf("No solution\n");
        else printf("%.2lf\n", mscore);


    }

    return 0;
}

习8-9 UVA 1613 K度图的着色

题意
输入一个n(3≤n≤9999)个点m条边(2≤m≤100000)的连通图,n保证为奇数。设k为最小的奇数,使得每个点的度数不超过k,你的任务是把图中的结点涂上颜色1~k,使得相邻结点的颜色不同。多解时输出任意解。输入保证有解。

思路
用贪心法做的。优先选择可选颜色少的点进行染色。如果先选可选颜色多的,其它的可选颜色少的进一步受到限制,到最后可能出现无法染色的情况。
尽管这样做能够AC,但在理论上无法证明这种方法的正确性。
我能够确定正确的做法是DFS+回溯,但这样做估计是会超时的。
此题留待以后进一步探讨。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 10000;
const int M = 100001;

int n, m, k;
vector<int> next[N];
int color[N], chosen[N];
int c[N];

void init()
{
    for (int i = 1; i <= n; i++)
        next[i].clear();
    memset(color, 0, sizeof(color));
    memset(chosen, 0, sizeof(chosen));
}

void get_k()
{
    k = 0;
    for (int i = 1; i <= n; i++)
        k = max(k, (int)next[i].size());
    if (k%2 == 0) k++;
}

int main(void)
{
    int kase = 0;
    while (scanf("%d%d", &n, &m) != EOF) {
        init();
        int a, b;
        for (int i = 0; i < m; i++) {
            scanf("%d%d", &a, &b);
            next[a].push_back(b);
            next[b].push_back(a);
        }

        get_k();

        for (int i = 1; i <= n; i++) {
            // 选择当前可选颜色最少的点p
            int p = 0, cnt = -1;
            for (int j = 1; j <= n; j++) {
                if (!color[j] && chosen[j] > cnt) {
                    p = j;
                    cnt = chosen[j];
                }
            }

            // 统计p的邻居已经选择的颜色
            memset(c, 0, sizeof(c));
            for (int j = 0; j < next[p].size(); j++) {
                c[color[next[p][j]]] = 1;
                chosen[next[p][j]]++;
            }

            // 从未选择的颜色中选择颜色
            int q;
            for (q = 1; q <= k; q++)
                if (!c[q]) break;
            color[p] = q;
        }

        if (kase++) printf("\n");
        printf("%d\n", k);
        for (int i = 1; i <= n; i++)
            printf("%d\n", color[i]);
    }

    return 0;
}

习8-10 UVA 1614 奇怪的股市

题意
输入一个长度为n(n≤100000)的序列a,满足1≤ai≤i,要求确定每个数的正负号,使得所有数的总和为0。例如a={1, 2, 3, 4},则设4个数的符号分别是1, -1, -1, 1即可(1-2-3+4=0),但如果a={1, 2, 3, 3},则无解(输出No)。

思路
如果序列和为奇数显然无解,而如果是偶数则一定有解。贪心法从大到小检查序列中的数即可。
至于贪心的正确性
证明一个结论吧,对于1≤ai≤i+1,一定可以表示出1~sum[i]中的任意一个数.
对于i=1显然成立,
假设对于i=k结论成立,那么对于i=k+1来说,只要证明sum[k]+i,1≤i≤ak+1可以凑出来就行了。
因为sum[k]+i≥k+1,且1≤ak+1≤k+1,所以可以先选一个ak+1,剩下的0≤sum[k]+i-ak+1≤sum[k]一定是可以有前面的数字凑出来的。
这就证明了贪心的正确性。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100001;

int n;
int a[N], c[N];

int main(void)
{
    while (scanf("%d", &n) != EOF) {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            sum += a[i];
        }
        if (sum&1) { printf("No\n"); continue; }

        sort(a, a+n);
        sum /= 2;
        int add = 0;
        memset(c, 0, sizeof(c));
        for (int i = n-1; i >= 0; i--) {
            if (add + a[i] <= sum) { add += a[i]; c[i] = 1;}
            if (add == sum) break;
        }

        printf("Yes\n");
        for (int i = 0; i < n; i++)
            printf("%d%c", c[i] ? 1 : -1, i == n-1 ? '\n' : ' ');
    }

    return 0;
}

习8-11 UVA 1615 高速公路

题意
给定平面上n(n≤10^5)个点和一个值D,要求在x轴上(0-L范围内)选出尽量少的点,使得对于给定的每个点,都有一个选出的点离它的欧几里德距离不超过D。

思路
对于每个点,求出x轴上(0-L范围内)与该点距离不超过D的区间。这样就将问题转化为用最少的点覆盖所有区间的问题。然后贪心法求解即可。

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;

const int N = 100001;

struct P {
    double l, r;
};

int n;
double len, d;
P p[N];

bool cmp(const P &a, const P &b)
{
    return a.l < b.l;
}

int solve()
{
    sort(p, p+n, cmp);

    int res = 0, i = 0;
    double posL, posR;
    while (i < n) {
        posL = p[i].l;
        posR = p[i].r;
        i++;
        while ( i < n && p[i].l <= posR ) {
            posL = p[i].l;
            posR = (p[i].r < posR) ? p[i].r : posR;
            i++;
        }
        res++;
    }
    return res;
}

int main(void)
{
    while (scanf("%lf%lf%d", &len, &d, &n) != EOF) {
        int flag = 1;
        double x, y;
        for (int i = 0; i < n; i++) {
            cin >> x >> y;
            double z = d*d - y*y;
            if (z < 0) flag = 0;
            z = sqrt(z);
            p[i].l = max(x - z, 0.0);
            p[i].r = min(x + z, len);
        }
        printf("%d\n", solve());
    }

    return 0;
}

习8-12 UVA 1153 顾客是上帝

题意

思路

代码

习8-13 UVA 10570 外星人聚会

题意
输入1~n的一个排列(3≤n≤500),每次可以交换两个整数。用最少的交换次数把排列变成1~n的一个环状排列。

思路
我的做法是暴力搜索+贪心选择。
将原排列分别循环左移0~(n-1)个位置,对移动后的排列贪心的将其变成1-n的升序排列,另外还有贪心的将其变成n-1的降序排列,分别统计最小移动次数,从而得到总的最小移动次数。

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 501;

int n;
int a0[N], a[N], c0[N], c[N];

int main(void)
{
    while (scanf("%d", &n) && n) {
        for (int i = 1; i <= n; i++)
            scanf("%d", &a0[i]);

        int res = n;
        for (int i = 0; i < n; i++) {
            for (int r = 0; r < 2; r++) {
                for (int j = 1; j <= n; j++) {
                    a[j] = a0[(j+i-1)%n+1];
                    c[a[j]] = j;
                }
                int cnt = 0;
                for (int j = 1; j <= n; j++) {
                    if (r == 0) {
                        if (a[j] != j) {
                            int k = a[j];
                            swap(a[j], a[c[j]]);
                            swap(c[k], c[j]);
                            cnt++;
                        }
                    } else {
                        int jj = n-j+1;
                        if (a[j] != jj) {
                            int k = a[j];
                            swap(a[j], a[c[jj]]);
                            swap(c[k], c[jj]);
                            cnt++;
                        }
                    }
                }
                res = min(res, cnt);
            }
        }
        printf("%d\n", res);
    }

    return 0;
}

习8-14 UVA 1616 商队抢劫者

题意
输入n条线段,把每条线段变成原线段的一条子线段,使得改变之后所有线段等长且不相交(但是端点可以重合)。输出最大长度(用分数表示)。例如,有3条线段[2,6],[1,4],[8,12],则最优方案是分别变成[3.5,6],[1,3.5],[8,10.5],输出5/2。
书中描述漏说的一个重要前提:对于任意i和j,不会同时满足 ai ≤ aj 且 bj ≤ bi。

思路
二分+贪心。
先用二分查找来搜索最大可行的长度。用贪心法来检查是否满足条件。设置迭代次数为100,足够了。
然后找到最大可行长度以后,枚举分母的所有情况,因为分母范围为1-n。

贪心法能够使用的条件是:
书中描述漏说的一个重要前提:对于任意i和j,不会同时满足 ai ≤ aj 且 bj ≤ bi。

该题的启示是:
如果想不出来如何找到解,首先看看给出一个解,如何去验证。
然后再想想如何找到解,比如使用二分查找。

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

const int N = 100001;
const int M = 1000000;

int n;
pair<int, int> a[N];

bool check(double mid)
{
    double begin = 0;
    for (int j = 0; j < n; j++) {
        if(a[j].first > begin)
            begin = a[j].first;
        if(begin + mid > a[j].second)
            return false;
        begin = begin + mid;
    }
    return true;
}

int main()
{
    while (scanf("%d", &n) != EOF) {
        for(int i = 0; i < n; i++)
            scanf("%d%d", &a[i].first, &a[i].second);
        sort(a, a+n);

        double lb = 0, ub = M, mid;
        for(int i = 1; i <= 100; i++) {
            mid = (lb+ub) / 2;
            if(check(mid)) lb = mid;
            else ub = mid;
        }

        int p = 0, q = 1;
        for(int i = 1; i <= n; i++) {
            int k = round(lb*i);
            if(fabs((double)k/i - lb) < fabs((double)p/q - lb)) {
                p = k; q = i;
            }
        }

        printf("%d/%d\n", p, q);
    }
    return 0;
}

习8-15 UVA 1617 笔记本

题意

思路

代码

习8-16 UVA 1618 弱键

题意

思路

代码

习8-17 UVA 11536 最短子序列

题意
有n(n≤10^6)个0~m-1(m≤1000)的整数组成一个序列。输入k(k≤100),你的任务是找一个尽量短的连续子序列(xa, xa+1, xa+2,…, xb-1, xb),使得该子序列包含1~k的所有整数。
例如,n=20,m=12,k=4,序列为1 (2 3 7 1 12 9 11 9 6 3 7 5 4) 5 3 1 10 3 3,括号内部分是最优解。如果不存在满足条件的连续子序列,输出sequence nai。

思路
滑动窗口法。
滑动搜索时需要有一个数组c统计1-k中每个数在当前窗口中出现的次数,另外有一个数num表示当前窗口中出现的1-k中不同数的个数。num的增减发生在某个数的统计次数从0变成1或相反时。
num=k时满足条件,与res比较并更新最短子序列。

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1000001;
const int K = 101;

int n, m, k;
int a[N], c[K];

int main()
{
    int kase;
    scanf("%d", &kase);

    for (int t = 1; t <= kase; t++) {
        scanf("%d%d%d", &n, &m, &k);
        for (int i = 0; i < n; i++) {
            if (i < 3) a[i] = i+1;
            else a[i] = (a[i-1]+a[i-2]+a[i-3])%m + 1;
        }

        memset(c, 0, sizeof(c));
        int num = 0, l = 0, r = 0, res = n+1;
        while (true) {
            while (r < n && num < k) {
                if (a[r] <= k && c[a[r]]++ == 0) num++;
                r++;
            }
            if (num < k) break;
            res = min(res, r-l);
            if (a[l] <= k && --c[a[l]] == 0) num--;
            l++;
        }

        printf("Case %d: ", t);
        if (res > n) printf("sequence nai\n");
        else printf("%d\n", res);
    }

    return 0;
}

习8-18 UVA 1619 感觉不错

题意
给出一个长度为n(n≤100000)的正整数序列ai,求出一段连续子序列al,…,ar, 使得(al+…+ar)*min{al,…,ar}尽量大。

思路
开始没有思路,看了别人的博客解析后豁然开朗。
既然所有数都是大于等于0的,那么在一个区间最小值一定的情况下,这个区间越长越好(当然最小值为0时特殊)。那么我们对每个a[i]都求出以它为最小值的周围最大区间即可
对一个数a[i],l[i]代表左边第一个比它小的数的位置(不存在则为-1),r[i]代表右边第一个比它小的位置(不存在则为n),则最大区间就是[l[i]+1, r[i]-1]。
如何构造l[i]呢?,从左往右构造一个单调递增的栈(注意一定是单调的!)。当a[i]比栈顶元素小的时候,栈顶元素出栈,直到栈为空或当前栈顶元素小于a[i]时,这就找到了l[i]。同时将a[i]入栈继续顺序搜索。
r[i]同理可求得。
最后求sum*min的最大值即可。这里求sum用到一个技巧是求出所有sum(1~k)(k从1-n),这样任意区间sum都可以用它来表示了。也就是说求任意区间的sum都是线性复杂度O(N)。详见代码。

另外,注意数组全零的特殊情况,如果写出来WA的话,试一下这组数据:
6
0 0 0 0 0 0
结果应该是
0
1 1
而不是
0
1 6

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<stack>
using namespace std;

const int N = 100001;

typedef long long LL;

int n;
int a[N];
LL sum[N];
int l[N], r[N];

stack<int> st;

int main()
{
    int kase = 0;
    while (scanf("%d", &n) != EOF) {
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            sum[i] = (i == 0) ? a[0] : (sum[i-1]+a[i]);
        }

        // get l[N]
        while (st.size()) st.pop();
        for (int i = 0; i < n; i++) {
            while (st.size() && a[st.top()] >= a[i]) st.pop();
            l[i] = st.empty() ? -1 : st.top();
            st.push(i);
        }

        // get r[N]
        while (st.size()) st.pop();
        for (int i = n-1; i >= 0; i--) {
            while (st.size() && a[st.top()] >= a[i]) st.pop();
            r[i] = st.empty() ? n : st.top();
            st.push(i);
        }

        // get result
        LL res = 0, cur;
        int ll = -1, rr = 1;
        for (int i = 0; i < n; i++) {
            if (l[i] == -1) cur = sum[r[i]-1] * a[i];
            else cur = (sum[r[i]-1]-sum[l[i]]) * a[i];
            if (cur > res || cur == res && r[i]-l[i] < rr - ll) {
                res = cur;
                ll = l[i];
                rr = r[i];
            }
        }

        if (kase++) printf("\n");
        printf("%lld\n%d %d\n", res, ll+2, rr);
    }

    return 0;
}

习8-19 UVA 1312 球场

题意

思路

代码

习8-20 UVA 1620 懒惰的苏珊

题意
把1~n(n≤500)放到一个圆盘里,每个数恰好出现一次。每次可以选4个连续的数字翻转顺序。问:是否能变成1, 2, 3,…, n的顺序?
提示:需要先奇偶分析排除无解的情况,然后写程序、找规律,或者手算得出有解时的构造算法。

思路
n为偶数或数组的逆序数为偶数就有解,否则无解。
看别人的博客学的,但不明白为什么。也不知道如何构造解。
此题存疑,以后再看。

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 501;

int n;
int a[N];

int main(int argc, char const *argv[])
{
    int kase;
    scanf("%d", &kase);
    while (kase--) {
        scanf("%d", &n);
        int cnt = 0;                                        
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            for (int j = 0; j < i; j++)                                                                                             
                if (a[j] > a[i]) cnt++;
        }
        if (!(cnt & 1) || !(n & 1)) printf("possible\n");
        else printf("impossible\n");
    }                                                                                                                                                           
    return 0;                                                                                                                                                       
}

习8-21 UVA 1621 跳来跳去

题意
你的任务是数轴上的0点出发,访问0, 1, 2,…, n各一次,在任意点终止。需要用票才能从一个点到达另一个点。有3种票,跳跃长度为1, 2, 3,分别有a, b, c张(3≤a,b,c≤5000),且n=a+b+c。每张票只能用一次。输入保证有解。
例如,a=3,b=4,c=3,则n=10,一种可能解为0->3->1->2->5->4->6->9->7->8->10,其中第1种票的3张分别用在1->2,5->4,7->8;第2种票的4张分别用在3->1,4->6,9->7,8->10;第3种票的3张分别用在0->3,2->5,6->9。

思路
对于这种稍复杂一点的构造性题目一向没有思路。所幸看到了一篇博客。
总体思路是:前后前折返再往前,分三段先走完步数为3的(根据不同情况需要1-3个步数为1或2的辅助),然后顺序走步数为1的只剩下一个,最后前后折返走步数为2的(辅助一个步数为1的)。

但此题最坑的是输入格式与其它题目都不一样。可能有多组多组数据!注意是两层多组!!

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int n, cur;
int a[4];
vector<int> res;

void move(int d, int cnt)
{
    for (int i = 0; i < cnt; i++) {
        cur += d;
        res.push_back(cur);
    }
    a[abs(d)] -= cnt;
}

int main()
{
    int kase;
    //freopen("in", "r", stdin);
    while (scanf("%d", &kase) != EOF) {
        while (kase--) {
            scanf("%d%d%d", &a[1], &a[2], &a[3]);
            n = a[1]+a[2]+a[3]+1;
            cur = 0;
            res.clear();
            res.push_back(0);

            int k = a[3]/3;
            if (a[3]%3 == 0) {
                move(3, k);
                move(1, 1);
                move(-3, k);
                move(1, 1);
                move(3, k);
            } else if (a[3]%3 == 1) {
                move(3, k+1);
                move(-2, 1);
                move(-3, k);
                move(1, 1);
                move(3, k);
                move(2, 1);
            } else {
                move(3, k+1);
                move(-1, 1);
                move(-3, k);
                move(-1, 1);
                move(3, k+1);
            }

            move(1, a[1]-1);
            k = (a[2]+1)/2;
            move(2, k);
            if (a[2] == k) move(1, 1);
            else move(-1, 1);
            move(-2, a[2]);

            for (int i = 0; i < res.size(); i++)
                printf("%d%c", res[i], i == res.size()-1 ? '\n' : ' ');
        }
    }

    return 0;
}

习8-22 UVA 1622 机器人

题意
有一个n*m(1≤n,m≤105)的网格,每个格子里都有一个机器人。每次可以发出如下4种指令之一:NORTH、SOUTH、EAST、WEST,作用是让所有机器人往相应方向走一格。如果一个机器人在执行某一命令后走出了网格,则它会立即炸毁。
给出4种指令的总条数(0≤CN,CS,CW,CE≤105),求一种指令顺序使得所有机器人执行的命令条数之和最大。炸毁的机器人不再执行命令。

思路
贪心法可解。
本以为这个题就是简单的贪心,不想各种WA,改了将近10次才最终AC。下面详细讲讲这道题我的做法以及需要注意的地方。

指令有四种:北南东西。由于网格初始是对称的,先对指令数进行调整交换,使北>南以及东>西。这样便于以后的处理(因为对于两个相反方向的指令,需要先走多的那种指令,后面我们会讲为什么这样处理)。
以P edge[2]表示网格中剩下的机器人的左上角和右下角坐标,对应于机器人的矩形区域。每次移动后,edge的坐标如果超出了网格范围,则只保留在网格范围内的坐标。根据edge坐标可求出矩形区域的面积,也就是矩形区域内机器人的个数,而面积的减少是我们不希望的,我们的目标是尽可能最小化面积减少量。
那么是不是在每次搜索时,贪心的选取面积减少量最小的指令就可以呢?这实际上是我最初的设想,事实证明没有那么简单。还有很多的情况需要考虑:

  1. 两个相反方向指令个数不相等时应先走指令数多的,相等时分别优先走北和东两个方向。比如北2个,南3个的情况。若先走南,之后交替走,则只有第一步发生面积减小,之后面积都不变;然而若先走北,之后即使交替走,最后两步也只能是南和南,也就是第一步和最后一步都会发生面积减小,总得面积减小值更大。实际上只考虑南北两个方向的时候(只考虑东西也是一样),必定要先走指令多的那个方向,然后交替走,最终只剩下指令多的那个方向。前面提过,对指令数预先进行了调整交换,使北>南以及东>西,那么如此走法,南一定先比北用完,西一定比东先用完。
  2. 南和西两个方向只有面积减少量为0时才会走。根据第一条可以得出此推论。且这一条是第三条的条件之一。
  3. 如果北和东两个方向都会发生面积减少,判断北和东两个方向那个先走时,所考察的面积减少量要在原基础上增加另外方向上行走一次后行走时面积不变的次数乘以面积减少量-1。这句话比较拗口,解释一下就是,如果先走北,当前面积减少量是lost,而另外的方向东行走后(也对应面积减少),能够西东交替行走且保持面积不变的次数为k,则综合考虑的面积减少量应当为lostfinal += lost+k*(lost-1)。这个不进一步解释了,有兴趣的读者自行思考吧。
  4. 只剩下北东两个方向时可以贪心的选取面积减少量最小的指令。从直观感觉来看,只剩这两个方向时,执行任意指令都会发生面积减少,那么不就应该先走面积减少量少的嘛?笔者不会证明,但确实编程简单验证了一下,是没问题的。
  5. 面积为0时应当退出。否则程序会发生计算错误。

这个题用了很长时间才完成,但最后完全是自己想出来的,还是比较有成就感的。最后提醒一下数据类型应该用long long。

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

typedef pair<int, int> P;
typedef long long LL;

P operator +(const P& a, const P& b)
{
    return P(a.first+b.first, a.second+b.second);
}

P get_min(const P& a, const P& b)
{
    return P(min(a.first, b.first), min(a.second, b.second));
}

P get_max(const P& a, const P& b)
{
    return P(max(a.first, b.first), max(a.second, b.second));
}

P d[4] = {P(-1, 0), P(1, 0), P(0, -1), P(0, 1)};

int n, m;
int cmd[4];

LL get_area(P edge[2])
{
    return (LL)(edge[1].first - edge[0].first) * (LL)(edge[1].second - edge[0].second);
}

int main()
{
    int kase = 0;
    int n, m;
    int cmd[4];

    //freopen("in", "r", stdin);
    while (scanf("%d%d", &n, &m), n || m) {
        for (int i = 0; i < 4; i++)
            scanf("%d", &cmd[i]);
        if (cmd[0] < cmd[1]) swap(cmd[0], cmd[1]);
        if (cmd[2] < cmd[3]) swap(cmd[2], cmd[3]);

        LL res = 0;
        P edge[2] = {P(0, 0), P(n, m)};
        LL area = get_area(edge);
        while (cmd[0] || cmd[1] || cmd[2] || cmd[3]) {
            int ans = -1;
            P edge1[2];
            LL area1, lost = 1LL<<62;
            for (int i = 0; i < 4; i++) {
                if (cmd[i]) {
                    edge1[0] = get_max(edge[0]+d[i], P(0, 0));
                    edge1[1] = get_min(edge[1]+d[i], P(n, m));
                    area1 = get_area(edge1);
                    LL lost1 = area - area1;
                    if (i%2 == 1 && lost1 > 0) continue;

                    if (lost1 > 0 && i%2 == 0) {
                        int j = (i+2)%4;
                        if (cmd[j] == cmd[j+1] && cmd[j] > 0)
                            lost1 += (cmd[j+1]*2-1)*(lost1-1);
                        else
                            lost1 += cmd[j+1]*2*(lost1-1);
                    }

                    if (lost1 < lost) {
                        lost = lost1;
                        ans = i;
                        if (lost == 0) break;
                    }
                }
            }
            cmd[ans]--;
            res += area;
            //printf("ans=%d, area=%lld\n", ans, area);
            edge[0] = get_max(edge[0]+d[ans], P(0, 0));
            edge[1] = get_min(edge[1]+d[ans], P(n, m));
            area = get_area(edge);
            if (area == 0) break;
        }
        printf("Case %d: %lld\n", ++kase, res);
    }   

    return 0;
}

习8-23 UVA 1623 神龙喝水

题意
某城市里有n个湖,每个湖都装满了水。天气预报显示不久的将来会有暴雨。具体来说,在接下来的m天内,每天要么不下雨,要么恰好往一个湖里下暴雨。如果这个湖里已经装满了水,将会引发水灾。为了避免水灾,市长请来一只神龙,可以在每个不下雨的天里喝干一个湖里的水(也可以不喝)。如果以后再往这个干枯的湖里下暴雨,湖会重新被填满,但不会引发水灾。神龙应当如何喝水才能避免水灾?n≤10^6,m≤10^6。

思路
看数据规模就知道至少应该要O(nlogn)的算法才能胜任,我的算法就是O(nlogn)的。
我的代码可能显得略复杂一些,但思路是很清晰的。
首先利用map找出数组w[M]中每个非零值的上一个等于该值的位置,相应存入l[M]数组。建立l数组的原因是,对于某天某个湖要下暴雨的情况,一定要在上一个该湖下暴雨之后找龙把水喝掉,而不能是之前。
next0(map数据结构)记录的是连续非零值的最后一个位置(作为key)后面的0的个数(作为value)。建立next0数组的目的是准备在key位置后面写值(对应于龙喝水)。
然后顺序根据每天的天气搜索,对于下雨的天气i,找出不小于l[i]的next0中的key,也就是说找到上一个在该湖下雨的时间往后第一个能够让龙喝水的位置(紧接着在key的后面)。赋值龙喝水的位置后相应的要让key对应的value减1,而value等于0时意味着没有可喝水位置,这个key就可以直接删掉了。
最后求出的ans就是所求结果。
注意每次循环开始都要将可能有影响的数组及map重新初始化。

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;

const int N = 1000001, M = N;

int n, m;
int w[M], l[M];
map<int, int> lastid;
map<int, int> next0;
int res[M];

void init()
{
    lastid.clear();
    next0.clear();
    memset(res, 0, sizeof(res));
}

int main()
{
    int kase;
    //freopen("in", "r", stdin);
    scanf("%d", &kase);
    while (kase--) {
        scanf("%d%d", &n, &m);
        init();
        for (int i = 1; i <= m; i++) {
            scanf("%d", &w[i]);
            if (w[i]) {
                if (lastid.count(w[i])) l[i] = lastid[w[i]];
                else l[i] = 0;
                lastid[w[i]] = i;
            }
        }

        int cnt = 0;
        for (int i = m; i >= 1; i--) {
            if (w[i]) { if (cnt) { next0[i] = cnt; cnt = 0; } }
            else cnt++;
        }
        if (cnt) next0[0] = cnt;

        bool ans = true;
        map<int, int>::iterator it;
        for (int i = 1; i <= m; i++) {
            if (w[i]) {
                it = next0.lower_bound(l[i]);
                if (it == next0.end()) {
                    ans = false; break;
                } else {
                    //printf("w[i]=%d, (%d, %d)\n", w[i], it->first, it->second);
                    int pos = it->first + it->second;
                    if (pos > i) {ans = false; break;}
                    res[pos] = w[i];
                    if ((--(it->second)) == 0) next0.erase(it);
                    //else printf("(%d, %d)\n", it->first, it->second);
                }
            }
        }

        if (ans) {
            printf("YES\n");
            bool first = true;
            for (int i = 1; i <= m; i++) {
                if (!w[i]) {
                    if (first) first = false;
                    else printf(" ");
                    printf("%d", res[i]);
                }
            }
            printf("\n");
        } else {
            printf("NO\n");
        }
    }

    return 0;
}

习8-24 UVA 10366 龙头滴水

题意

思路

代码

习8-25 UVA 11175 有向图D到E

题意

思路

代码

习8-26 UVA 12559 找黑圈

题意

思路

代码

习8-27 UVA 1580 海盗的宝箱

题意

思路

代码

习8-28 UVA 1624 打结

题意

思路

代码

你可能感兴趣的:(算法,ACM,uva,算法竞赛入门经典)