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

文章目录

  • 说明
  • 例题
    • 例8-1 UVA 120 煎饼
    • 例8-2 UVA 1605 联合国大楼
    • 例8-3 UVA 1152 和为 0 的 4 个值
    • 例8-4 UVA 11134 传说中的车
    • 例8-5 UVA 11054 Gergovia 的酒交易
    • 例8-6 UVA 1606 两亲性分子(未尝试)
    • 例8-7 UVA 11572 唯一的雪花
    • 例8-8 UVA 1471 防线(未尝试)
    • 例8-9 UVA 1451 平均值(未尝试)
    • 例8-10 UVA 714 抄书
    • 例8-11 UVA 10954 全部相加
    • 例8-12 UVA 12627 奇怪的气球膨胀
    • 例8-13 UVA 11093 环形跑道
    • 例8-14 UVA 1607 与非门
    • 例8-15 UVA 12174 乱序播放记录
    • 例8-16 UVA 1608 不无聊的序列
    • 例8-17 UVA 1609 不公平竞赛(未尝试)
    • 例8-18 UVA 1442 洞穴
    • 例8-19 UVA 12265 贩卖土地(未尝试)

说明

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

例题

例8-1 UVA 120 煎饼

题意
有一叠煎饼正在锅里。煎饼共有n(n≤30)张,每张都有一个数字,代表它的大小。厨师每次可以选择一个数k,把从锅底开始数第k张上面的煎饼全部翻过来,即原来在上面的煎饼现在到了下面。
设计一种方法使得所有煎饼按照从小到大排序(最上面的煎饼最小)。输入时,各个煎饼按照从上到下的顺序给出。

思路
这道题目要求排序,但是基本操作却是“颠倒一个连续子序列”。不过没有关系,我们还是可以按照选择排序的思想,以从大到小的顺序依次把每个数排到正确的位置。方法是先翻到最上面,然后翻到正确的位置。由于是按照从大到小的顺序处理,当处理第i大的煎饼时,是不会影响到第1, 2, 3,…, i-1大的煎饼的(它们已经正确地翻到了煎饼堆底部的i-1个位置上)。

开始马虎了,将if (j > 1)判断条件不慎写成了if (j < 1)提交后WA了两发,提醒自己一定要细心啊!

代码

#include
#include
#include
#include
#include
#include
#include
using namespace std;

int n;
int a[31];

void flip(int k)
{
    for (int i = 1; i <= k/2; i++)
      swap(a[i], a[k-i+1]);
}

int main()
{
    string s;
    //freopen("in", "r", stdin);
    while (getline(cin, s)) {
        stringstream ss(s);
        int x;
        n = 0;
        while (ss >> x)
          a[++n] = x;

        int b[31];
        memcpy(b, a, sizeof(a));
        sort(b+1, b+n+1);

        vector res;
        for (int i = n; i >= 1; i--) {
            if (a[i] != b[i]) {
                int j = 1;
                for (; a[j] != b[i]; j++);
                if (j > 1) {flip(j); res.push_back(n-j+1);}
                flip(i); res.push_back(n-i+1);
            }
        }

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

    return 0;
}

例8-2 UVA 1605 联合国大楼

题意
你的任务是设计一个包含若干层的联合国大楼,其中每层都是一个等大的网格。有若干国家需要在联合国大楼里办公,你需要把每个格子分配给一个国家,使得任意两个不同的国家都有一对相邻的格子(要么是同层中有公共边的格子,要么是相邻层的同一个格子)。你设计的大厦最多不能超过1000000个格子。
输入国家的个数n(n≤50),输出大楼的层数H、每层楼的行数W和列数L,然后是每层楼的平面图。不同国家用不同的大小写字母表示。

思路
本题的限制非常少,层数、行数和列数都可以任选。正因为如此,本题的解法非常多。我采用的是书中给出的解法:一共只有两层,每层都是n*n的,第一层第i行全是国家i,第二层第j列全是国家j。

代码

#include
#include
#include
#include
using namespace std;

char ans[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

int main()
{
    int n;
    while (scanf("%d", &n) != EOF){
        printf("2 %d %d\n", n, n);
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++)
              printf("%c", ans[i]);
            printf("\n");
        }
        printf("\n");
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++)
              printf("%c", ans[j]);
            printf("\n");
        }
    }
    return 0;
}

例8-3 UVA 1152 和为 0 的 4 个值

题意
给定4个n(1≤n≤4000)元素集合A, B, C, D,要求分别从中选取一个元素a, b, c, d,使得
a+b+c+d=0。问:有多少种选法?

思路
中途相遇法。这是一种特殊的算法,大体思路是从两个不同的方向来解决问题,最终“汇集”到一起。

最容易想到的算法就是写一个四重循环枚举a, b, c, d,看看加起来是否等于0,时间复杂度为O(n4),超时。一个稍好的方法是枚举a, b, c,则只需要在集合D里找找是否有元素-a-bc,如果存在,则方案加1。如果排序后使用二分查找,时间复杂度为(n3logn)。
把刚才的方法加以推广,就可以得到一个更快的算法:首先枚举a和b,把所有a+b记录下来放在一个有序数组或者STL的map里,然后枚举c和d,查一查-c-d有多少种方法写成a+b的形式。两个步骤都是O(n2logn),总时间复杂度也是O(n2logn)。

代码

#include 
#include 
#include 
using namespace std;

const int N = 4000;

int n, m;
int a[4][N];
int x[N*N], y[N*N];

int main(void)
{
    int kase;
    cin >> kase;
    while (kase--) {
        cin >> n;
        m = n*n;
        for (int i = 0; i < n; i ++)
          for (int j = 0; j < 4; j ++)
            scanf("%d", &a[j][i]);
        for (int i = 0; i < n; i ++)
          for (int j = 0; j < n; j ++)
            x[i*n+j] = a[0][i] + a[1][j];
        for (int i = 0; i < n; i ++)
          for (int j = 0; j < n; j ++)
            y[i*n+j] = a[2][i] + a[3][j];
        sort(y, y+m);

        long long ans = 0;
        for (int i = 0; i < m; i ++)
          ans += (upper_bound(y, y+m, -x[i]) - lower_bound(y, y+m, -x[i]));
        printf("%lld\n", ans);
        if (kase) printf("\n");
    }

    return 0;
}

例8-4 UVA 11134 传说中的车

题意
你的任务是在n*n的棋盘上放n(n≤5000)个车,使得任意两个车不相互攻击,且第i个车在一个给定的矩形Ri之内。用4个整数xli, yli, xri, yri(1≤xli≤xri≤n,1≤yli≤yri≤n)描述第i个矩形,其中(xli,yli)是左上角坐标,(xri,yri)是右下角坐标,则第i个车的位置(x,y)必须满足xli≤x≤xri,yli≤y≤yri。如果无解,输出IMPOSSIBLE;否则输出n行,依次为第1,2,…,n个车的坐标。

思路
两个车相互攻击的条件是处于同一行或者同一列,因此不相互攻击的条件就是不在同一行,也不在同一列。可以看出:行和列是无关的,因此可以把原题分解成两个一维问题。
在区间[1~n]内选择n个不同的整数,使得第i个整数在闭区间[n1i, n2i]内。贪心法可解。复杂度O(n^2).

代码

#include
#include
#include
#include
#include
using namespace std;

const int N = 5001;

int n;
int rangeA[2][N], rangeB[2][N];
int pos[2][N];

bool put_rook(int i)
{
    int *ra = rangeA[i], *rb = rangeB[i];
    int *p = pos[i];

    int c[N];
    memset(c, 0, sizeof(c));
    for (int j = 1; j <= n; j++) {
        int mink = n+1, minb = n+1;
        for (int k = 1; k <= n; k++) {
            if (!c[k] && ra[k] <= j && rb[k] >= j && rb[k] < minb) {
                mink = k;
                minb = rb[k];
            }
        }
        //printf("j=%d, mink=%d, ra[mink]=%d, rb[mink]=%d\n", j, mink, ra[mink], rb[mink]);
        if (mink == n+1) return false;
        p[mink] = j;
        c[mink] = 1;
    }
    return true;
}

int main()
{
    while (scanf("%d", &n) && n) {
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < 2; j++)
                  scanf("%d", &rangeA[j][i]);
            for (int j = 0; j < 2; j++)
                  scanf("%d", &rangeB[j][i]);
        }

        if (!put_rook(0) || !put_rook(1)) printf("IMPOSSIBLE\n");
        else {
            for (int i = 1; i <= n; i++)
              printf("%d %d\n", pos[0][i], pos[1][i]);
        }
    }

    return 0;
}

例8-5 UVA 11054 Gergovia 的酒交易

题意
直线上有n(2≤n≤100000)个等距的村庄,每个村庄要么买酒,要么卖酒。设第i个村庄对酒的需求为ai(-1000≤ai≤1000),其中ai>0表示买酒,ai<0表示卖酒。所有村庄供需平衡,即所有ai之和等于0。
把k个单位的酒从一个村庄运到相邻村庄需要k个单位的劳动力。计算最少需要多少劳动力可以满足所有村庄的需求。输出保证在64位带符号整数的范围内。

思路
考虑最左边的村庄。如果需要买酒,即a1>0,则一定有劳动力从村庄2往左运给村庄1,而不管这些酒是从哪里来的(可能就是村庄2产的,也可能是更右边的村庄运到村庄2的)。这样,问题就等价于只有村庄2~n,且第2个村庄的需求为a1+a2的情形。不难发现,ai<0时这个推理也成立(劳动力同样需要|ai|个单位).

代码

#include
#include
#include
#include
using namespace std;

int main( ) {
    int n;
    while(cin >> n && n) {
        long long ans = 0, a, last = 0;
        for(int i = 0; i < n; i++) {
            cin >> a;
            ans += abs(last);
            last += a;
        }
        cout << ans << "\n";
    }
    return 0;
}

例8-6 UVA 1606 两亲性分子(未尝试)

题意

思路

代码



例8-7 UVA 11572 唯一的雪花

题意
输入一个长度为n(n≤106)的序列A,找到一个尽量长的连续子序列AL~AR,使得该序列中没有相同的元素。

思路
假设序列元素从0开始编号,所求连续子序列的左端点为L,右端点为R。首先考虑起点L=0的情况。可以从R=0开始不断增加R,相当于把所求序列的右端点往右延伸。当无法延伸(即A[R+1]在子序列A[L~R]中出现过)时,只需增大L,并且继续延伸R。既然当前的A[L~R]是可行解,L增大之后必然还是可行解,所以不必减少R,继续增大即可。
不难发现这个算法是正确的,不过真正有意思的是算法的时间复杂度。暂时先不考虑“判断是否可以延伸”这个部分,每次要么把R加1,要么把L加1,而L和R最多从0增加到n-1,所以指针增加的次数是O(n)的。
最后考虑“判断是否可以延伸”这个部分。比较容易想到的方法是用一个STL的set,保存A[L~R]中元素的集合,当R增大时判断A[R+1]是否在set中出现,而R加1时把A[R+1]插入到set中,L+1时把A[L]从set中删除。因为set的插入删除和查找都是O(logn)的,所以这个算法的时间复杂度为O(nlogn)。

代码

#include
#include
#include
#include
#include
using namespace std;

const int N = 1000001;

int n;
int a[N];

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

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

        int ans = 1, l = 0, r = 0;
        set sa;
        while (true) {
            while (r < n && !sa.count(a[r]))
              sa.insert(a[r++]);
            ans = max(ans, r-l);
            if (r == n) break;
            sa.erase(a[l++]);
        }
        printf("%d\n", ans);
    }

    return 0;
}

例8-8 UVA 1471 防线(未尝试)

题意

思路

代码



例8-9 UVA 1451 平均值(未尝试)

题意

思路

代码



例8-10 UVA 714 抄书

题意
把一个包含m个正整数的序列划分成k个(1≤k≤m≤500)非空的连续子序列,使得每个正整数恰好属于一个序列。设第i个序列的各数之和为S(i),你的任务是让所有S(i)的最大值尽量小。例如,序列1 2 3 2 5 4划分成3个序列的最优方案为1 2 3 | 2 5 | 4,其中S(1)、S(2)、S(3)分别为6、7、4,最大值为7;如果划分成1 2 | 3 2 | 5 4,则最大值为9,不如刚才的好。每个整数不超过107。如果有多解,S(1)应尽量小。如果仍然有多解,S(2)应尽量小,依此类推。

思路
“最大值尽量小”是一种很常见的优化目标。下面考虑一个新的问题:能否把输入序列划分成m个连续的子序列,使得所有S(i)均不超过x?将这个问题的答案用谓词P(x)表示,则让P(x)为真的最小x就是原题的答案。P(x)并不难计算,每次尽量往右划分即可(想一想,为什么)。
接下来又可以猜数字了——随便猜一个x0,如果P(x0)为假,那么答案比x0大;如果P(x0)为真,则答案小于或等于x0。至此,解法已经得出:二分最小值x,把优化问题转化为判定问题P(x)。设所有数之和为M,则二分次数为O(logM),计算P(x)的时间复杂度为O(n)(从左到右扫描一次即可),因此总时间复杂度为O(nlogM)(4)。

此题值得注意的地方是如何使得越靠前的划分值越小。我的实现方法是将其转化为从后往前搜索,使得越靠后的划分的划分值越大。详见代码。

代码

#include 
#include 
#include 
#include 
using namespace std;

const int N = 501;
const int INF = 10000000;

typedef long long LL;

int n, k;
int a[N];

bool check(LL mid)
{
    int cnt = 1;
    LL sum = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] > mid) return false;
        if (sum + a[i] > mid) {
            cnt++;
            sum = a[i];
        } else
            sum += a[i];
    }
    return cnt <= k;
}

void get_div(vector& div, LL mid)
{
    int cnt = 1;
    LL sum = 0;
    for (int i = n-1; i >= 0; i--) {
        if (i < k-cnt || sum + a[i] > mid) {
            cnt++;
            sum = a[i];
            div.push_back(i+1);
        } else
            sum += a[i];
    }
}

int main(void)
{
    int kase;
    cin >> kase;
    while (kase--) {
        cin >> n >> k;
        LL sum = 0;
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            sum += a[i];
        }

        LL lb = 0, ub = sum;
        while (ub - lb > 1) {
            LL mid = (lb + ub) / 2;
            if (check(mid)) ub = mid;
            else lb = mid;
        }

        vector div;
        get_div(div, ub);
        int j = k-2;
        for (int i = 0; i < n; i++) {
            if (div[j] == i) { printf("/ "); j--;}
            printf("%d%c", a[i], i == n-1 ? '\n' : ' ');
        }
    }

    return 0;
}

例8-11 UVA 10954 全部相加

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

思路
这不就是Huffman编码的建立过程吗?因为n比较小,还可以采用一种更容易写的方法——使用一个优先队列。

代码

#include
#include
#include
#include
using namespace std;
        
int main(void)
{
    int n; 
    while(cin >> n && n)
    {
        priority_queue, greater > Queue;

        for(int i = 1; i <= n; i++)
        {
            int temp;
            scanf("%d", &temp);
            Queue.push(temp);
        }

        int mincost = 0;
        while (Queue.size() > 1)
        {
            int a = Queue.top();
            Queue.pop();
            int b = Queue.top();
            Queue.pop();

            Queue.push(a+b);
            mincost += a+b;
        }

        printf("%d\n", mincost);

        while(!Queue.empty())
            Queue.pop();
    }
    return 0;
}

例8-12 UVA 12627 奇怪的气球膨胀

题意
一开始有一个红气球。每小时后,一个红气球会变成3个红气球和一个蓝气球,而一个蓝气球会变成4个蓝气球,如图8-19所示分别是经过0, 1, 2, 3小时后的情况。经过k小时后,第A~B行一共有多少个红气球?例如,k=3,A=3,B=7,答案为14。

思路
k小时的情况由4个k-1小时的情况拼成,其中右下角全是蓝气球,不用考虑。剩下的3个部分有一个共同点:都是前k-1小时后“最下面若干行”或者“最上面若干行”的红气球总数。
具体来说,设f(k, i)表示k小时之后最上面i行的红气球总数,g(k,i)表示k小时之后最下面i行的红气球总数(规定i≤0时f(k,i)=g(k,i)=0),则所求答案为f(k,b) - f(k, a-1)。
如何计算f(k,i)和g(k,i)呢?以g(k,i)为例,下面分两种情况进行讨论.
如果i≥2k-1,则g(k,i)=2g(k-1,i-2k-1)+c(k),否则g(k,i)=g(k-1,i)。其中,c(k)表示k小时后红气球的总数,满足递推式c(k)=3c(k-1),而c(0)=1,因此c(k)=3k。
不管是哪种情况,g(k,i)都可以直接转化为k-1的情况,因此g(k,i)的计算时间为O(k)。类似地,f(k,i)的计算时间也是O(k),因此本题的总时间复杂度为O(k)。

我开始的做法是直接递归求解f(k, a, b),也就是k小时后a行与b行之间的红气球总数。递推式大概是f(k, a, b) = 2*f(k-1, a1, b1) + f(k-1, a2, b2)的样子,一提交TLE了。分析了一下,这个递推式的时间复杂度是O(2^k)的,远远超出线性复杂度!然后改成书中提到的递归方式就AC了。

代码

#include 
#include 
#include 
#include 
using namespace std;

typedef long long LL;

int exp2[31];
LL exp3[31];

LL f1(int k, int a) //up
{
    if (a == 0) return 0;
    if (k == 0) return 1;
    if (a <= exp2[k-1]) return 2*f1(k-1, a);
    else return 2*exp3[k-1] + f1(k-1, a-exp2[k-1]);
}

LL f2(int k, int b) //up
{
    if (b == exp2[k]+1) return 0;
    if (k == 0) return 1;
    if (b > exp2[k-1]) return f2(k-1, b-exp2[k-1]);
    else return exp3[k-1] + 2*f2(k-1, b);
}

int main(void)
{
    exp2[0] = exp3[0] = 1;
    for (int i = 1; i <= 30; i++) {
        exp2[i] = exp2[i-1]*2;
        exp3[i] = exp3[i-1]*3;
    }

    int kase;
    scanf("%d", &kase);
    for (int t = 1; t <= kase; t++) {
        int k, a, b;
        scanf("%d%d%d", &k, &a, &b);
        printf("Case %d: %lld\n", t, exp3[k] - f1(k, a-1) - f2(k, b+1));
    }

    return 0;
}

例8-13 UVA 11093 环形跑道

题意
环形跑道上有n(n≤100000)个加油站,编号为1~n。第i个加油站可以加油pi加仑。从加油站i开到下一站需要qi加仑汽油。你可以选择一个加油站作为起点,初始油箱为空(但可以立即加油)。你的任务是选择一个起点,使得可以走完一圈后回到起点。假定油箱中的油量没有上限。如果无解,输出Not possible,否则输出可以作为起点的最小加油站编号。

思路
考虑1号加油站,直接模拟判断它是否为解。如果是,直接输出;如果不是,说明在模拟的过程中遇到了某个加油站p,在从它开到加油站p+1时油没了。这样,以2, 3,…, p为起点也一定不是解(想一想,为什么)。这样,使用简单的枚举法便解决了问题,时间复杂度为O(n)。

代码

#include
#include
#include
#include
using namespace std;

const int N = 100001;

int n;
int p[N], q[N];

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

    for (int t = 1; t <= kase; t++) {
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
            scanf("%d", &p[i]);
        for (int i = 0; i < n; i++)
            scanf("%d", &q[i]);

        int bg = 0;
        while (bg < n) {
            int i = bg, sp = 0, sq = 0;
            for (; i < bg+n; i++) {
                sp += p[i%n], sq += q[i%n];
                if (sp < sq) break;
            }
            if (i == bg+n) break;
            bg = i+1;
        }

        printf("Case %d: ", t);
        if (bg < n) printf("Possible from station %d\n", bg+1);
        else printf("Not possible\n");
    }

    return 0;
}

例8-14 UVA 1607 与非门

题意
可以用与非门(NAND)来设计逻辑电路。每个NAND门有两个输入端,输出为两个输入端与非运算的结果。即输出0当且仅当两个输入都是1。给出一个由m(m≤200000)个NAND组成的无环电路,电路的所有n个输入(n≤100000)全部连接到一个相同的输入x。
请把其中一些输入设置为常数,用最少的x完成相同功能。输出任意方案即可。

思路
因为只有一个输入x,所以整个电路的功能不外乎4种:常数0、常数1、x及非x。先把x设为0,再把x设为1,如果二者的输出相同,整个电路肯定是常数,任意输出一种方案即可。
如果x=0和x=1的输出不同,说明电路的功能是x或者非x,解至少等于1。不妨设x=0时输出0,x=1时输出1。现在把第一个输入改成1,其他仍设为0(记这样的输入为1000…0),如果输出是1,则得到了一个解x000…0。
如果1000…0的输出也是0,再把输入改成1100…0,如果输出是1,则又得到了一个解1x00…0。如果输出还是0,再尝试1110…0,如此等等。由于输入全1时输出为1,这个算法一定会成功。
问题在于m太大,而每次“给定输入计算输出”都需要O(m)时间,逐个尝试会很慢。好在已经学习了二分查找:只需二分1的个数,即可在O(Logm)次计算之内得到结果,总时间复杂度为O(mlogm)。

如果电路输出非常数,则一定有一个关键的x能够决定结果。问题在于找到这个关键的x。二分法可解。
这个题需要加深理解,目前我还没有完全理解透。

代码

#include
#include
#include
#include
using namespace std;

const int N = 100001;
const int M = 200001;

int n, m;
bool in[N], out[M];
int gate[M][2];

bool get_out(int k)
{
    fill(in+1, in+k+1, 1);
    fill(in+k+1, in+n+1, 0);

    bool a, b;
    for (int i = 1; i <= m; i++) {
        a = gate[i][0] < 0 ? in[-gate[i][0]] : out[gate[i][0]];
        b = gate[i][1] < 0 ? in[-gate[i][1]] : out[gate[i][1]];
        out[i] = !(a&&b);
    }
    return out[m];
}

void print(int posx)
{
    for (int i = 1; i < posx; i++)
      printf("1");
    if (posx) printf("x");
    for (int i = posx+1; i <= n; i++)
      printf("0");
    printf("\n");
}

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

        int posx = 0;
        int res[2];
        res[0] = get_out(0);
        res[1] = get_out(n);
        if (res[0] != res[1]) {
            int lb = 0, ub = n;
            while (ub - lb > 1) {
                int mid = (lb + ub) / 2;
                if (get_out(mid) == res[1]) ub = mid;
                else lb = mid;
            }
            posx = ub;
            printf("%d\n", ub);
        }

        print(posx);
    }

    return 0;
}

例8-15 UVA 12174 乱序播放记录

题意
你正在使用的音乐播放器有一个所谓的乱序功能,即随机打乱歌曲的播放顺序。假设一共有s首歌,则一开始会给这s首歌随机排序,全部播放完毕后再重新随机排序、继续播放,依此类推。注意,当s首歌播放完毕之前不会重新排序。这样,播放记录里的每s首歌都是1~s的一个排列。
给出一个长度为n(1≤s,n≤100000)的播放记录(不一定是从最开始记录的)xi(1≤xi≤s),你的任务是统计下次随机排序所发生的时间有多少种可能性。
例如,s=4,播放记录是3, 4, 4, 1, 3, 2, 1, 2, 3, 4,不难发现只有一种可能性:前两首是一个段的最后两首歌,后面是两个完整的段,因此答案是1;当s=3时,播放记录1, 2, 1有两种可能:第一首是一个段,后两首是另一段;前两首是一段,最后一首是另一段。答案为2。

思路
“连续的s个数”让你联想到了什么?没错,滑动窗口!这次的窗口大小是“基本”固定的(因为还需要考虑不完整的段),因此只需要一个指针;而且所有数都是1~s的整数,也不需要STL的set,只需要一个数组即可保存每个数在窗口中出现的次数。再用一个变量记录在窗口中恰好出现一次的数的个数,则可以在O(n)时间内判断出每个窗口是否满足要求(每个整数最多出现一次)。
这样,就可以枚举所有可能的答案,判断它对应的所有窗口,当且仅当所有窗口均满足要求时这个答案是可行的。

此题的思路非常明确,但在实际编码中却没那么顺利。s和n不一定那个大那个小,滑动窗口的大小有可能是小于s的,要考虑到各种情况,很容易漏考虑了某种情况导致WA。
代码不解释了,给出一组测试数据帮没有AC的人:
INPUT

1
6 4
1 4 6 4

OUTPUT

2

代码

#include
#include
#include
#include
using namespace std;

const int N = 100001;

int s, n;
int a[N], c[N];
bool flag[N];

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

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

        memset(c, 0, sizeof(c));
        memset(flag, true, sizeof(flag));
        int num = 0;
        for (int i = 0; i < n+s; i++) {
            if (i < n && !(c[a[i]]++)) num++;
            if (i >= s && !(--c[a[i-s]])) num--;
            if (i < s) {
                if (num < min(n, i+1)) flag[i%s] = false;
            } else if (i < n) {
                if (num < s) flag[i%s] = false;
            } else if (num < n-max(i-s+1, 0)) {
                flag[i%s] = false;
            }
        }

        num = 0;
        for (int i = 0; i < s; i++)
          if (flag[i]) num++;
        printf("%d\n", num);
    }

    return 0;
}

例8-16 UVA 1608 不无聊的序列

题意
如果一个序列的任意连续子序列中至少有一个只出现一次的元素,则称这个序列是不无聊(non-boring)的。输入一个n(n≤200000)个元素的序列A(各个元素均为109以内的非负整数),判断它是不是不无聊的。

思路
不难想到整体思路:在整个序列中找一个只出现一次的元素,如果不存在,则这个序列不是不无聊的;如果找到一个只出现一次的元素A[p],则只需检查A[1…p-1]和A[p+1…n]是否满足条件。
如何找唯一元素?如果事先算出每个元素左边和右边最近的相同元素(还记得《唯一的雪花》吗?),则可以在O(1)时间内判断在任意一个连续子序列中,某个元素是否唯一。
但从左往右找和从右往左找的最坏情况下时间复杂度是O(n^2),而从两边往中间找的时间复杂度则为O(nlogn),详细分析见书中。

代码

#include 
#include 
#include 
#include 
#include 
using namespace std;

const int N = 200001;

int n;
int a[N], lnext[N], rnext[N];

bool check(int l, int r)
{
    if (r-l < 1) return true;

    for (int i = 0; i <= (r-l)/2; i++) {
        int ll = l+i;
        if (lnext[ll] < l && rnext[ll] > r)
          if (check(l, ll-1) && check(ll+1, r)) return true;
        int rr = r-i;
        if (rr == ll) break;
        if (lnext[rr] < l && rnext[rr] > r)
          if (check(l, rr-1) && check(rr+1, r)) return true;
    }
    return false;
}

int main(void)
{
    int kase;
    scanf("%d", &kase);
    for (int t = 1; t <= kase; t++) {
        scanf("%d", &n);
        map mp;
        mp.clear();
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            lnext[i] = mp.count(a[i]) ? mp[a[i]] : -1;
            mp[a[i]] = i;
        }
        mp.clear();
        for (int i = n-1; i >= 0; i--) {
            rnext[i] = mp.count(a[i]) ? mp[a[i]] : n;
            mp[a[i]] = i;
        }

        if (check(0, n-1)) printf("non-boring\n");
        else printf("boring\n");
    }

    return 0;
}

例8-17 UVA 1609 不公平竞赛(未尝试)

题意

思路

代码



例8-18 UVA 1442 洞穴

题意

思路
分两个方向分别扫描,然后结合起来,方法非常巧妙。

代码

#include 
#include 
#include 
#include 
using namespace std;

#define FOR1(i, a, b) for (int i = (a); i <= (b); i++)
#define FOR2(i, a, b) for (int i = (a); i >= (b); i--)

const int MAXN = 1000001;

int n;
int P[MAXN], S[MAXN], H1[MAXN], H2[MAXN];

int main() {
#ifdef CODE_LIANG
	freopen("datain.txt", "r", stdin);
	freopen("dataout.txt", "w", stdout);
#endif
	int T;
	scanf("%d", &T);
	FOR1(t, 1, T) {
		scanf("%d", &n);
		FOR1(i, 0, n - 1) scanf("%d", &P[i]);
		FOR1(i, 0, n - 1) scanf("%d", &S[i]);

		H1[0] = S[0];
		FOR1(i, 1, n - 1) {
			H1[i] = min(H1[i-1], S[i]);
			H1[i] = max(H1[i], P[i]);
		}
		H2[n-1] = S[n-1];
		FOR2(i, n-2, 0) {
			H2[i] = min(H2[i + 1], S[i]);
			H2[i] = max(H2[i], P[i]);
		}
				
		int ans = 0;
		FOR1(i, 0, n - 1) ans += min(H1[i], H2[i]) - P[i]; // 注意H1和H2要分别扫描(why?)
		printf("%d\n", ans);
	}

	return 0;
}

例8-19 UVA 12265 贩卖土地(未尝试)

题意

思路

代码



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