【題目全解】ACGO挑戰賽#8

Macw發表於2024-09-04

前言:本次挑戰賽的難度相較於前面幾期有所提升,主要還是因為集訓的關係,出題組的成員們沒有充裕的時間想原創題目(so,只能原模原樣搬運某一場 ABC 的考試了。)Anyway,AK 了就行。

備註:由於 Python 的常數過大,本題解暫不同步更新 Python 版本的題解。

第一題 - Intersection

題目跳轉:交集

這道題可以用暴力的方法,也可以稍微動點腦筋。看在資料量小,直接打暴力就行了,沒必要花時間去找兩個線段之間的關係。

注意到 \(0 \le L_1 R_1, L_2, R_2 \le 100\),可以新建一個標記陣列:如果某個座標屬於某一條線段,那麼就將對應索引的標記增加一。最後再遍歷一邊標記陣列,如果存在一個座標被標記了兩遍,那麼就證明該座標被兩條線段同時覆蓋住了。

最後注意輸出的時候要 \(-1\),表示區間的長度。(如果沒有任何重合的區間,即 ans == 0 時,特判直接輸出 \(0\) 就可以了。

程式碼時間複雜度:\(O(N)\)

本題的 AC 程式碼如下:

#include <iostream>
using namespace std;

int a, b, c, d;
int arr[1005], ans;

int main(){
    cin >> a >> b >> c >> d;
    for (int i=a; i<=b; i++) 
        arr[i]++;
    for (int i=c; i<=d; i++) 
        arr[i]++;
    for (int i=0; i<=100; i++)
        ans += (arr[i] == 2);
    cout << max(0, ans-1) << endl;
	return 0;
}

第二題 - Tournament Result

題目跳轉:比賽結果

第二題也是一道純暴力的模擬題目,雙層 for 迴圈依照題目題乾的要求判斷點 \((i, j)\) 和點 \((j, i)\)​​ 所存的值是否存在衝突就行了。這邊我用了一個 check 函式來檢查是否存在衝突。如果存在衝突直接返回結果即可。

這邊判斷衝突的 check 函式我覺得值得講一下,分類討論:

  1. 如果雙方勝負狀態相同且不是平局,則返回 false
  2. 如果一方是平局二另一方不是平局,則返回 false
  3. 可以證明其餘的情況一定是合法的,直接返回 true 即可,不需要跟其他題解一樣大費周章來寫條件分支語句。

程式碼時間複雜度:\(O(N^2)\)

本題的 AC 程式碼如下:

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

int n;
char arr[1005][1005];

bool check(char a, char b){
    if (a == b && a != 'D') 
        return false;
    if (a == 'D' && b != a)
        return false;
    return true;
}

int main(){
    cin >> n;
    for (int i=1; i<=n; i++)
        for (int j=1; j<=n; j++)
            cin >> arr[i][j];
    for (int i=1; i<=n; i++){
        for (int j=1; j<=n; j++){
            // 當 i == j 的時候,跳過列舉。
            if (i == j) continue;
            if (!check(arr[i][j], arr[j][i])){
                cout << "incorrect" << endl;
                return 0;
            }
        }
    }
    cout << "correct" << endl;
    return 0;
}

第三題 - NewFolder(1)

題目跳轉:新建資料夾(1)

也是一道水題,用 STL 的 unordered_map/ map 存一下某一個字串出現的次數就可以了。由於 \(N\) 比較大,因此如果直接用雙層迴圈遍歷的話絕對會超時。

但需要注意的是,map 的單次插入/查詢的時間複雜度約為 \(O(\log_2 N)\)。因此本題的綜合時間複雜度約為 \(O(N \log_2 N)\)

本題的 AC 程式碼如下:

#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;

int n;
string str[200005];
unordered_map<string, int> map;

int main(){
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> str[i];
        map[str[i]] += 1;
        int cnt = map[str[i]];
        if (cnt == 1)
            cout << str[i] << endl;
        else 
            cout << str[i] << "(" << cnt-1 << ")" << endl;
    }
    return 0;
}

第四題 - Flipping and Bonus

題目跳轉:投硬幣

接下來來到了本次比賽的重頭戲,題目難度也在此有了一個質的飛躍(我也不清楚這次 ABC 的難度為什麼這麼跳躍)。

考慮使用動態規劃,定義狀態 \(dp_i\) 表示第 \(i\) 次擲硬幣可以獲得的最大金額。(其實這道題的狀態定義有很多種,用二維的 dp 也可以透過本題,本題解暫是隻提供字首和動態規劃的解法)

我們可以列舉在第 \(j\) 次拋置硬幣時選擇正面和反面的價值並取最大值。所以,對於每一次投擲硬幣有兩種可能的決策,分別是:

  1. 硬幣最終的狀態時正面,得到第 \(i\) 次投擲硬幣的錢,計數器自增。因此結果就是得到 \(x_1 + x_2 + x_3 + \cdots + x_{i-1} + x_i\) 元錢和 \(y_1 + y_2 + y_3 + \cdots + y_{k-1} + y_k\ (c[k] \le i)\) 的獎金。
  2. 硬幣最終的狀態是反面,那麼計數器就從頭開始計數。因此最終可以獲得的結果就為從第 \(j\) 次投硬幣時可以獲得的最大金額 \(dp_j\) 加上在區間 \([j+2, i]\) 投幣全是正面所獲的的金額(因為第 \(j+1\) 次投幣不會獲得任何的獎勵)\((x_1 + x_2 + x_3 + \dots + x_{i-1} + x_i) - (x_1 + x_2 + x_3 + \cdots + x_j + x_{j+1})\) 再加上前 \(i-j-1\) 次計數器的獎勵金額 \((y_1 + y_2 + y_3 + \cdots + y_{k-1} + y_k\ (c[k] \le i-j-1)\)

可以發現,我們可以透過開設兩個字首和陣列來最佳化演算法,分別約定 \(\mathtt{suma, sumb}\) 分別表示前 \(i\) 次投幣全都是正面所獲的的金額和計數器的獎勵金額。因此最後的狀態轉移方程時可以寫成:

\[dp_i = \max(dp_i, dp_j + suma_i - suma_{j+1} + sumb_{i-j-1}) \]

備註:十年 OI 一場空,不開 long long 見祖宗。請大家務必開 long long。本題的時間複雜度為 \(\Theta(\dfrac{N+N^2}{2})\)

周後,本題的 AC 程式碼如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
long long dp[5005],x[5005],c[5005],y[5005],suma[5005],sumb[5005],m,n,cnt;
signed main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>x[i];
        suma[i]=suma[i-1]+x[i];
    }
    for(int i=1;i<=m;i++){
        int a, b;
        cin >> a >> b;
        c[a] = b;
    }
    for (int i=1; i<=5000; i++){
        sumb[i]=sumb[i-1]+c[i];
    }
    for(int i=1;i<=n;i++){
        dp[i]=suma[i]+sumb[i];
        for(int j=1;j<i;j++){
            // 狀態轉移方程的推導見原文。
        	dp[i]=max(dp[i],dp[j]+suma[i]-suma[j+1]+sumb[i-j-1]);
        }
    }
    cout<<dp[n];
	return 0;
}

第五題 - Many Operations

題目跳轉:宏量運算

一道位運算的噁心題目,跟上一題一樣,可以用字首和動態規劃的思想來解決。因為每一位都是相互獨立的,所以只需要按位進行 \(\mathtt{dp}\) 預處理就可以了。定義狀態 \(dp_{i, j(0/1), k}\) 表示對於第 \(i\) 位來說,一開始的值為 \(j(0/1)\),經過前 \(k\) 次操作後這一位的值。而狀態轉移方程就是 \(dp_{i, j, k} = dp_{i, j, k-1}\)。之後不斷迭代就可以求出最後的解。

本題的主要難點是位運算的一些操作,有些同學對位運算的操作不太熟悉,這裡提供一些常見的位運算操作:

  1. 獲取一個數字的第 \(j\) 位:(x >> j) & 1
  2. 判斷數字是否是奇數:x & 1
  3. 將一個數字乘上 \(2^j\)(1 << j) * x

本題的時間複雜度約為 \(\Theta(60N)\)

因此,本題的 AC 程式碼如下:

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

const int N = 2e5 + 5;
int n, c;
int dp[50][5][N];
int t[N], a[N];

signed main(){
    cin >> n >> c;
    for (int i=1; i<=n; i++)
        cin >> t[i] >> a[i];
    // cout << log2(1e9) << endl; 
    for (int i=0; i<30; i++){
        for (int j=0; j<2; j++){
            dp[i][j][0] = j;
            for (int k=1; k<=n; k++){
                // 三種狀態,分別判斷一下就可以了了。
                bool x = a[k] & (1 << i);
                if (t[k] == 1) dp[i][j][k] = dp[i][j][k-1] & x;
                else if (t[k] == 2) dp[i][j][k] = dp[i][j][k-1] | x;
                else dp[i][j][k] = dp[i][j][k-1] ^ x;
            }
        }
    }
    for (int i=1; i<=n; i++){
        int ans = 0, k = c;
        for (int j=0; j<30; j++) {
            // 基礎位運算操作。
            ans += dp[j][k & 1][i] * (1 << j);
            k >>= 1;
        }
        cout << ans << endl;
        c = ans;
    }
    return 0;
}

第六題 - Sorting Color Balls

題目跳轉:綵球排序

一道逆序對的題目,因為我懶的使用歸併排序來做,所以我用了 樹狀陣列 + map 的方式,榮獲執行時長 44.9s 的好成績。其實這道題應該比第五題簡單,Based on my opinion。

一道普普通通的逆序對的題目,在計算逆序對的時候去除相同顏色的逆序對就可以了,類似一道模板題。我用了兩個樹狀陣列分別來計算每個數字出現的次數和每個顏色出現的次數。

本題的 AC 程式碼如下,時間複雜度約為 \(O(N \log_2 N)\)

#include <iostream>
#include <vector>
#include <unordered_map>
#define int long long
using namespace std;

const int N = 3e5 + 5;
int n, c[N], x[N];
int cnt[N], tot[N];
unordered_map<int, int> cnt2[N];

// 記錄某一個數字出現的次數。
void add_n(int x){
    while(x <= n){
        cnt[x] += 1;
        x += x & (-x);
    }
}

// 記錄某一個顏色出現的次數。
void add_c(int x, int c){
    while(x <= n){
        cnt2[x][c] += 1;
        x += x & (-x);
    }
}

// 查詢某一個數字出現的次數。
int query_n(int x){
    int ans = 0;
    while(x){
        ans += cnt[x];
        x -= x & (-x);
    }
    return ans;
}

// 查詢某一個顏色出現的次數。
int query_c(int x, int c){
    int ans = 0;
    while(x){
        ans += cnt2[x][c];
        x -= x & (-x);
    }
    return ans;
}

signed main(){
    cin >> n;
    for (int i=1; i<=n; i++) cin >> c[i];
    for (int j=1; j<=n; j++) cin >> x[j];
    int result = 0;
    for (int i=1; i<=n; i++){
        // 減去相同顏色出現的次數即可。
        result += (i - 1) - tot[c[i]] - query_n(x[i]) + query_c(x[i], c[i]);
        tot[c[i]] += 1;
        add_c(x[i], c[i]); add_n(x[i]);
    }
    cout << result << endl;
	return 0;
}

相關文章