【解題報告】Codeforces Round #401 (Div. 2)

題目鏈接


A. Shell Game(Codeforces 777A)

思路

爲了更好地找到解題關鍵,先將小球隨杯子運動的軌跡畫在紙上。具體地,假設初始狀態球在杯子 1 中。在紙上畫一個數組 d ,其中 d[i] 表示當杯子被移動 i 次時,小球的位置。不難發現, d[i]i 的增大呈現出週期性,其週期爲 6 。對任意初始狀態都可以畫出這種數組。
於是我們用二維數組 d[i][j] 表示初始狀態下小球在杯子 i 中,當杯子被移動了 j 次時小球的位置。對任意輸入 n ,枚舉i使得 d[i][n]=x ,那麼 i 就是答案。

代碼

#include <bits/stdc++.h>
using namespace std;

int n, x;
int d[3][6] = { {0, 1, 2, 2, 1, 0},
        {1, 0, 0, 1, 2, 2},
        {2, 2, 1, 0, 0, 1} };

int main() {
//  freopen("data.txt", "r", stdin);
    cin >> n >> x;
    n = n % 6;
    for(int i = 0; i < 3; i++) {
        if(d[i][n] == x) {
            cout << i << endl;
            break;
        }
    }
    return 0;
}

B. Game of Credit Cards(Codeforces 777B)

思路

首先這個數據規模暴力肯定是不行了。那麼潛在的,可能有貪心策略也可能是動態規劃。那麼先考察有沒有貪心策略。因爲兩個字符串的序對本題是沒有影響的(因爲M已經知道S的出牌順序)。因此考慮對兩個字符串進行排序。我們分兩種策略來考慮:

  • S 攻擊的最少次數。假設我們是 M ,那麼我們的目標是讓對方的牌儘可能不發揮作用。我們從當前 S 的點數最大的 S[i] 這兒考慮。爲了讓 S[i] 啞火,我們可以拿出最大的 M[j] 來壓制它。這樣爲什麼是最好的呢?因爲如果 M[j] 都壓不住它,那麼也沒有牌能夠壓住它了。況且如果此時不用 M[j] ,有什麼理由以後再用它呢?有人可能會反駁:能不能放棄壓制 S[i] ,把 M[j] 留到後面用呢?假如這樣做,那麼最好的情況是結果不變,最差的情況是結果變大了(也就是次優解)。因此我們按照點數從大到小枚舉 S[i] ,用當前最大的牌來壓制對方即可。
  • 攻擊 S 的最多次數。假設我們是 M ,那我們的目標是讓我們的牌儘可能發揮作用。那麼我們從 M 的點數最大的 M[j] 這兒考慮。爲了讓 M[j] 發揮最大效果,我們選擇比 M[j] 小的(或相等的)最大的 S[i] 下手。因爲對於任意滿足 S[k]S[i]k ,都存在 S[k]M[j] ,那麼將 M[j] 用在 S[i] 上最利於我方的後來的牌的發揮。我們同樣可以從大到小枚舉 S[j] ,用當前最大的牌來壓制對方。

代碼

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1010;
char S[maxn], M[maxn];
int n, Max, Min;

int main() {
//  freopen("data.txt", "r", stdin);
    scanf("%d%s%s", &n, S, M);
    sort(S, S + n);
    sort(M, M + n);
    int j = n - 1;
    for(int i = n - 1; i >= 0; i--) {
        if(S[i] > M[j]) {
            Min++;
        }
        else {
            j--;
        }
    }
    j = n - 1;
    for(int i = n - 1; i >= 0; i--) {
        if(S[i] < M[j]) {
            Max++;
            j--;
        }
    }
    printf("%d\n%d\n", Min, Max);
    return 0;
}

C. Alyona and Spreadsheet(Codeforces 777C)

思路

對於這題,不少人都能夠立即想到暴力的解法:對於每列 j ,檢查行區間 [l,r] 中的每行,看看這些行是不是在 j 列上單調不減。這是不錯的想法,但是時間複雜度太大了,達到了 O(nm+n+m)
事實上,這種要對矩陣進行某種統計,暴力解複雜度太大的題,多半是要維護某種神奇的量,使得我們只用枚舉行或枚舉列就能完成這種統計。話說 CF 出這種題已經不是一次兩次了。
那麼這題怎麼樣呢?我們可以事先統計 d[j][i] ,表示以 ij 列的元素 aij 爲終點的最長上升子串(注意是子串而不是子序列)的長度爲多少。此時原問題就轉化成

對於第 r 行,是否存在一個 j ,使得 rd[j][r]+1l

不等式左邊的式子表示以 r 爲區間右端點 d[j][r] 爲區間長度的區間左端點的編號(也就是本題的行號)是多少。將式子變形後問題轉化成

是否存在一個 j ,使得 d[j][r]rl+1

顯然這個問題就是在問

max{d[j][r]}rl+1 是否成立。

那麼對於每個 r ,維護 d[j][r] 的最大值即可。這既是我前面所說的神奇的量

最後,實現上需要注意一個細節。因爲題目只說 n×m105 ,所以理論上說 n,m 都有可能達到 105 。在存矩陣的時候如果用二維數組的話會超內存限制。所以得采用類似圖論中鄰接表的數據結構才能存下。

代碼

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 10;
vector <int> a[maxn], d[maxn];
int n, m, k, l, r, num, Max[maxn];

int main() {
//  freopen("data.txt", "r", stdin);
    scanf("%d%d", &n, &m);
    // 輸入並存儲矩陣
    for(int i = 1; i <= n; i++) {
        a[i].push_back(0);
        for(int j = 1; j <= m; j++) {
            scanf("%d", &num);
            a[i].push_back(num);
        }
    }
    for(int j = 1; j <= m; j++) {
        d[j].push_back(0);
        d[j].push_back(1);
    }
    // 對每列計算以某個元素結尾的最長不下降子串
    for(int j = 1; j <= m; j++) {
        for(int i = 2; i <= n; i++) {
            if(a[i][j] >= a[i-1][j]) {
                d[j].push_back(d[j][i-1] + 1);
            }
            else {
                d[j].push_back(1);
            }
        }
    }
    // 統計每行的列最長不下降子串的最大值
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= m; j++) {
            Max[i] = max(Max[i], d[j][i]);
        }
    }
    // 回答查詢
    scanf("%d", &k);
    while(k--) {
        scanf("%d%d", &l, &r);
        puts(r - Max[r] + 1 <= l ? "Yes" : "No");
    }
    return 0;
}

D. Cloud of Hashtags(Codeforces 777D)

思路

這題從數據規模和“字典序”這個這麼強的條件來看,可能存在貪心策略。首先從先往後考慮,結果是沒什麼結果。那麼從後往前考慮呢?首先,對於一個字符串,我們刪除它的後綴只會讓它的字典序變小而不是變大。那麼,對於倒數第一個串 s[n] 和倒數第二個串 s[n1] ,如果字典序 s[n]<s[n1] ,那麼我們對 s[n] 做任何事情都於事無補,但可以刪除 s[n1] 的後綴。
從這個靈感不難得出貪心算法:我們從第一位開始逐位比較兩個字符串 s[i]s[i1]

  • 如果出現某位j使得 s[i][j]>s[i1][j] ,那麼一切順利。
  • 如果出現某位j使得 s[i][j]<s[i1][j] ,那麼從 j 開始的 s[i1] 的後綴要全部刪除掉。
  • 如果一直是 s[i][j]==s[i1][j] 直到某個串被遍歷完,那麼只需要 s[i] 的長度大於或等於 s[i1] 的長度即可。

代碼

#include <bits/stdc++.h>
using namespace std;

const int maxn = 5e5 + 10;
string s[maxn];
bool equ;
int n, p1, p2;

int main() {
//  freopen("data.txt", "r", stdin);
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> s[i];
    }
    for(int i = n - 1; i >= 1; i--) {
        equ = true;
        p1 = p2 = 0;
        while(true) {
            if(s[i+1][p1] < s[i][p2]) {
                if(true == equ) {
                    s[i] = s[i].substr(0, p2);
                }
                break;
            }
            if(s[i+1][p1] > s[i][p2]) {
                equ = false;
            }
            if(++p2 >= s[i].size()) {
                break;
            }
            if(++p1 >= s[i+1].size()) {
                if(true == equ) {
                    s[i] = s[i].substr(0, p2);
                }
                break;
            }
        }
    }
    for(int i = 1; i <= n; i++) {
        cout << s[i] << endl;
    }
    return 0;
}

E. Hanoi Factory(Codeforces 777E)

思路

這題顯然不是貪心能夠解決的(很難找到某個順序進行某種貪心策略)。考慮動態規劃。我們需要有一個“序”來讓動規滿足“無後效性”。根據題目的特點先按照外徑從大到小對 ring 排序,當外徑相等時按照內徑從大到小排序(相當於排在前面的一定放在下面)。這時候就可以設計動態規劃算法了。
令狀態 i 表示當前考慮到 ringi 這個物品,且 ringi 是放在塔頂的物品, d[i] 表示狀態 i 下的最優解。若某個狀態 j 可以轉移到狀態 i ,那麼物品 ringi 的外徑一定要嚴格小於 ringj 的外徑,且物品 ringi 的外徑一定要嚴格大於 ringj 的內徑,用方程來描述狀態轉移過程中最優解的變化有( a,b,h 數組分別存放內徑,外徑和高度)

d[i]=max{d[j],j<i,b[i]>a[j]}

這看上去會是一個 O(n2) 的算法。讓我們來嘗試優化它(所以一定要熟悉動態規劃及其優化技巧,這樣才能自信認爲這樣的優化會奏效)。根據 j<i 這個信息。我們可以開一個數組 data[] ,假設我們處理到狀態 i 了, data[k] 表示在狀態 i 之前出現過的內徑爲 k 的物品在塔的最頂端時的最優解。那麼我們就可以通過區間最大值查詢 RMQ(1,b[i]1) ,來得知 max{d[j],j<i,b[i]>a[j]} (相當於不需要知道 j ,而直接知道了 d[j] ),從而更新 d[i] ,然後通過 d[i] 來更新 data[a[i]] 。那麼這有什麼用呢?將問題逐步變形至此有什麼好處呢?答案是,動態的 RMQ 以及修改元素是可以用線段樹在 O(logn) 的時間內完成的(需要對內徑離散化)。只要將 data 數組用線段樹維護起來就好了。這樣,本題的複雜度達到了 O(nlogn) ,這是可以接受的。

代碼

#include <bits/stdc++.h>
using namespace std;

#define lch (k << 1)
#define rch (k << 1 | 1)
#define mid ((l + r) >> 1)

typedef long long ll;
const int maxn = 2e5 + 10;
map <int, int> mp;
int n, a, b, h, m, foo[maxn], bar[maxn];
ll d, tmp, ans;

// 排序用的ring結構體
struct ring {
    int a, b, h;
    ring() {}
    ring(int a, int b, int h):a(a), b(b), h(h) {}
    bool operator < (const ring& o) const {
        if(b == o.b) {
            return a > o.a;
        }
        return b > o.b;
    }
}rings[maxn];

// 線段樹
template <class T>
struct Tree {
    T data[maxn<<2];
    T operate(T x, T y) {
        return max(x, y);
    }
    void pushUp(int k) {
        data[k] = operate(data[lch], data[rch]);
    }
    // 建樹
    void build(int k, int l, int r) {
        if(l == r) {
            data[k] = 0;
            return;
        }
        build(lch, l, mid);
        build(rch, mid + 1, r);
        pushUp(k);
    }
    // 修改
    void update(int a, T v, int k, int l, int r) {
        if(l == r) {
            data[k] = v;
            return;
        }
        if(a <= mid) {
            update(a, v, lch, l, mid);
        }
        else {
            update(a, v, rch, mid + 1, r);
        }
        pushUp(k);
    }
    // 查詢
    T query(int a, int b, int k, int l, int r) {
        if(a <= l && r <= b) {
            return data[k];
        }
        ll res = 0;
        if(a <= mid) {
            res = operate(res, query(a, b, lch, l, mid));
        }
        if(b > mid) {
            res = operate(res, query(a, b, rch, mid + 1, r));
        }
        return res;
    }
};

Tree <ll> o;

// 離散化
int dec(int a[], int b[], int n, map <int, int>& mp) {
    copy(a + 1, a + n + 1, b + 1);
    sort(b + 1, b + n + 1);
    int m = unique(b + 1, b + n + 1) - b - 1;
    for(int i = 1; i <= n; i++) {
        mp[a[i]] = lower_bound(b + 1, b + m + 1, a[i]) - b;
    }
    return m;
}

int main() {
//  freopen("data.txt", "r", stdin);
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%d%d%d", &a, &b, &h);
        rings[i] = ring(a, b, h);
        foo[i] = a;
        foo[n + i] = b;
    }
    // 離散化
    m = dec(foo, bar, 2 * n, mp);
    o.build(1, 1, m);
    sort(rings + 1, rings + n + 1);
    for(int i = 1; i <= n; i++) {
        a = rings[i].a;
        b = rings[i].b;
        h = rings[i].h;
        // 查詢d[j]
        if(mp[b] >= 2) {
            d = o.query(1, mp[b] - 1, 1, 1, m) + h;
        }
        else {
            d = h;
        }
        // tmp相當於d[i]
        tmp = o.query(mp[a], mp[a], 1, 1, m);
        // 將d[i]插入線段樹
        if(d > tmp) {
            o.update(mp[a], d, 1, 1, m);
        }
        // 用d[i]更新答案
        ans = max(ans, d);
    }
    printf("%I64d\n", ans);
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章