1. 程式人生 > >演算法競賽入門經典(第二版)-劉汝佳-第八章 高效演算法設計 習題(18/28)

演算法競賽入門經典(第二版)-劉汝佳-第八章 高效演算法設計 習題(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 球場

題意

思路

程式碼