【題目全解】ACGO排位賽#12

Macw發表於2024-09-11

ACGO 排位賽#12 - 題目解析

別問為什麼沒有挑戰賽#11,因為挑戰賽#11被貪心的 Yuilice 吃掉了(不是)。

本次挑戰賽難度相比較前面幾次有所提升。

爆料:小魚現在已經入職了研發部門,排位賽的算分級制將在未來的幾場排位賽中做出重大改變。之後就不會出現大家段位都很低的情況了。對於程度好的同學,稍微打一兩把排位賽段位就會有很大的提升。

第一題 - 50%𝐴𝐼, 50%𝐻𝑢𝑚𝑎𝑛

題目連結跳轉:點選跳轉

STL 大法真好,用自帶的 string.size() 方法就可以快速求出一個字串的長度。具體思路見程式碼,根據題目要求模擬就好了。

本題的 AC 程式碼如下:

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

/*
PS: 信心倍增題目。
寫完這道題會讓你覺得這場比賽特別簡單。但其實不是。
*/

int n, cnt;
string str;

int main(){
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> str;
        if (str.size() > 5) cnt++;
    }
    int ans = ceil(1.0 * cnt / n * 100);
    printf("%d%%AI, %d%%Human\n", ans, 100 - ans);
    return 0;
}

第二題 - 統計區間內奇數與偶數的數量

題目連結跳轉:點選跳轉

對於這種區間的問題,可以先考慮一部分。如果要求出從 \([1, L]\) 區間內滿足條件的數字我們應該怎麼辦?假設 \(L\) 是一個偶數,那麼 \([1, L]\) 區間的奇數和偶數就應該是 \(L / 2\)。相同地,假設 \(L\) 是一個奇數,那麼 \([1, L]\) 區間的奇數和偶數就分別是 \(L / 2 + 1\)\(L / 2\)。注意到 \(L / 2 + 1\)\(L / 2\) 兩個公式都可以被簡寫成 \((L + 1) / 2\)

\(odd(L)\)\(even(L)\) 分別為區間 \([1, L]\) 的奇數個數和偶數個數。那麼可以得出結論,如果要求 \([L, R]\) 區間的奇數個數,答案就應該是 \(odd(R) - odd(L-1)\)\(even(R) - even(L-1)\)

本題的 AC 程式碼如下,程式碼並沒有使用函式,見諒:

#include <iostream>
using namespace std;

/*
思維水題。
稍微想一下找找規律就好了。
*/

int q, l, r, k;

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> q;
    while(q--){
        cin >> k >> l >> r;
        if (k == 1) cout << ((r + 1) >> 1) - ((l) >> 1) << endl;
        else cout << (r >> 1) - ((l - 1) >> 1) << endl;
    }
    return 0;
}

第三題 - 火星揹包 II

題目連結跳轉:點選跳轉

一道反向 \(0/1\) 揹包的模板題目。通常,\(0/1\) 揹包問題的目標是選擇若干物品,使得這些物品的總重量不超過揹包容量,並且這些物品的總價值最大化。而反向 \(0/1\) 揹包問題則是反其道而行之,其目標是選擇若干物品,使得這些物品的總重量不低於給定的最小值,並且這些物品的總價值最小化。

主要就是狀態的定義比較難:設 \(dp[j]\) 表示恰好湊出總重量為 \(j\) 的最小代價。那麼我們可以得到如下的狀態轉移方程:

\[dp_j = \min(dp_j, dp_{j-w_i} + v_i); \]

根據狀態的定義,我們將 \(dp\) 陣列一開始全部初始化為正無窮大,同時另 \(dp_0 = 0\),表示恰好湊出重量為 \(0\) 的物品的最小代價為 \(0\),正無窮大代表暫時還沒有答案,需要在後續的迴圈中更新。

之後就是跑一遍正常的 Knapsack 01 程式碼就好了。在輸出答案的時候,我們需要找到一個滿足條件 \((dp[i] \le m)\)​ 的最大值。用一個 for 迴圈就可以搞定了。

本題的 AC 程式碼如下:

#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;

/*
反向 01 揹包題目。
問題不大,X03 的同學們在集訓營做過類似的題目(maybe 僅限杭州八月一期)。
*/

int n, m, sum;
int dp[100005], ans;
int w[1005], v[1005];

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m;
    for (int i=1; i<=n; i++){
        cin >> w[i] >> v[i];
        sum += v[i];
    }
    memset(dp, 0x7f, sizeof dp);
    dp[0] = 0;
    for (int i=1; i<=n; i++){
        for (int j=sum; j>=v[i]; j--){
            dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    for (int i=1; i<=sum; i++){
        if (dp[i] <= m) ans = i;
    }
    cout << ans << endl;
    return 0;
}

第四題 - 可分數列

題目連結跳轉:點選跳轉

題目很簡單,但需要仔細想一會兒才行。這道題的解決思路就是 找規律!沒錯,就是找一下規律就好了。我們只需要關注如何將這 \(4N + 2\) 個數列在去掉兩個元素後可以被平均分成 \(N\) 個等差數列。

透過手動模擬的方式,注意到我們只需要把這些數字豎著排列就可以解決問題。另每一列有四個元素,但其中有兩列有五個元素(代表 \(4N + 2\)\(+2\) 部分),共有 \(N\) 列。例如數列 \([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]\)。豎著排列之後就是這個樣子的:

1 7 13 19 25
3 9 15 21 27
5 11 17 23

可以看到這些元素構成了三個等差數列,且兩個等差數列的長度為 \(5\)。那麼對於長度為 \(4\) 的等差數列我們完全可以不用管,直接輸出就好了。而對於這兩個長度為 \(5\) 的等差數列,我們考慮從這兩個等差數列中各刪除一個數字來構成新的等差數列。綜合來看,我們發現刪除元素 \(3\)\(25\) 後,新的兩個等差數列依舊滿足題目限定的條件。

再多列舉幾個例子,發現規律也是相通的。那麼因此我們可以得出結論,對於任意一個長度為 \(4N + 2\) 的等差數列,只需要刪除這個等差數列的第 \(2\) 項和第 \(4N + 1\) 項,剩下的 \(4N\) 個元素一定可以組成 \(N\) 個長度為 \(4\) 的等差數列。

關於本題的更多資訊,可以閱讀 アイドル 老師的題解作為補充:連結跳轉

找到規律後程式碼就很好寫了,本題的 AC 程式碼如下:

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

int n, x, d;

signed main(){
    cin >> n >> x >> d;
    if (n == 1){
        cout << -1 << endl;
        return 0;
    }
    cout << 2 << " " << 4 * n + 1 << endl;
    for (int i=0; i<n; i++){
        int t = x + i * d;
        if (i == 1) t += n * d;
        for (int j=0; j<4; j++)
            cout << t + j * n * d << " ";
        cout << endl;
    }
    return 0;
}

第五題 - 花火大會

題目連結跳轉:點選跳轉

這道題的輸出不是故意為難大家,因為輸出實在太大了,超出了 CF 限定了 64MB 的限制,因此只能將輸出地圖改為了輸出地圖的雜湊值(對於沒有學過雜湊的同學來說,輸出答案也是一個比較困難的事情)。

本道題的難度其實不大,就是一個多源不同權的無權最短路問題。但由於資料量比較大,使用普通的優先佇列維護會超時(別問我是怎麼知道的,我一開始就用了優先佇列,然後在 #13 測試點就 TLE 了),因此我們需要考慮如何找到一個 workaround 來替換優先佇列。

注意到我們只需要另外再使用一個 while,在每次獲取頭節點的時候判斷某一個煙花是否剛好在該時間點燃放,如果煙花正好燃放,我們就把這個煙花的座標加入到佇列之中。

while(arr[cnt].z <= t.z && cnt <= k){
    vis[arr[cnt].x][arr[cnt].y] = 1;
    que.push(arr[cnt]); cnt ++;
}

由於每一個煙花每一個單位時間只會影響附近的一個格子,說明在放入煙花的時候佇列中隊頭和隊尾的權重是相同的,這樣就保證佇列內的元素權重一定是單調遞增的。這這種情況下,這道題就轉換成了使用 BFS 來求多源無權最短路的條件。

本題的 AC 程式碼如下:

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int MOD = 998244353;

int n, m, k, cnt = 2;
struct node{
    int x, y, z;
} arr[1005]; 
queue<node> que;
int dis[5005][5005];
int vis[5005][5005];
int dx[] = {0, 1, -1, 0};
int dy[] = {1, 0, 0, -1};

bool cmp(node a, node b){
    return a.z < b.z;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);

    cin >> n >> m >> k;
    for (int i=1, x, y, z; i<=k; i++)
        cin >> arr[i].x >> arr[i].y >> arr[i].z;

    sort(arr+1, arr+1+k, cmp);
    que.push(arr[1]);
    vis[arr[1].x][arr[1].y] = 1;

    while(!que.empty()){
        node t = que.front();
        que.pop();
        if (dis[t.x][t.y]) continue;
        dis[t.x][t.y] = t.z;
        while(arr[cnt].z <= t.z && cnt <= k){
            vis[arr[cnt].x][arr[cnt].y] = 1;
            que.push(arr[cnt]); cnt ++;
        }
        for (int i=0; i<4; i++){
            int cx = dx[i] + t.x;
            int cy = dy[i] + t.y;
            if (cx < 1 || cy < 1 || cx > n || cy > m) continue;
            if (dis[cx][cy]) continue; 
            if (vis[cx][cy]) continue;
            vis[cx][cy] = 1;
            que.push((node){cx, cy, t.z+1});
        }
    }
    
    long long ans = 0, p = 1;
    for (int i=1; i<=m; i++) 
        p = (p * 233) % MOD;
    for (int i=1; i<=n; i++){
        for (int j=1; j<=m; j++){
            p = (p * 233) % MOD;
            ans = (ans + (dis[i][j] * p) % MOD) % MOD;
        }
    }

    cout << ans << endl;
    return 0;
}

第六題 - 劍之試煉

題目連結跳轉:點選跳轉

這道題超級噁心,我發自內心地對那些在比賽過程中 AC 此題目的同學表示尊敬。

思路上非常好想,由於所有的怪獸都要打,且打每一個怪獸的時間都是固定的,因此我們可以在一開始就先預處理出打完所有怪獸的時間,需要最佳化的就只有趕路的時間了。

先考慮每一層的情況,對於任意一層來說,關鍵點就是找到一個最優的路徑(從某一個起點出發,經過所有的點後再傳送到下一層的最短路徑)即可。學習過狀態壓縮動態規劃的同學可以很容易想到模板題【最短 Hamilton 路徑】(洛谷連結:連結跳轉)。

對於每一層來說,具體的實現方法和函式如下:

  1. cover(dist, grid, "#*");

    • distgrid中標記為"#""*"的所有障礙物進行覆蓋處理。這一步的作用是標記出所有不可通行的區域,為後續的距離計算或路徑規劃提供基礎。
  2. getMinDist(dist, grid, "#");

    • 計算從當前所在位置到grid中標記為"#"(障礙物)位置的最短距離。這一步的作用是為後續的路徑規劃獲取到障礙物的距離資訊,方便下一步進行路徑選擇。
  3. hamilton(dist, grid);

    • distgrid上執行哈密頓路徑(Hamiltonian Path)的計算,目的是找到一條經過所有目標點的路徑。這一步的作用是根據前面的距離資訊,找到一條經過所有必經點的路徑。
  4. cover(dist, grid, "#.");

    • distgrid中標記為"#"(障礙物)和"."(空地)的部分進行覆蓋處理。這一步是為了確保後續的距離計算排除已標記的障礙物,並識別可通行的路徑。
  5. getMinDist(dist, grid, "#");

    • 再次計算從當前所在位置到grid中標記為"#"的最短距離。這一步是為了更新經過路徑覆蓋處理後的最短距離,確保得到最新的距離資訊。

除此之外就是一些小的細節最佳化了,具體請參閱程式碼註釋。

本題的 AC 程式碼如下(本程式碼參考了黃老師的 std,並加以修改(我是不會說我陣列傳來傳去把自己傳死了,討厭指標的一天)),若需要更詳細的解答,可以 參考本文

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

int n, m, k;
string map[30][105];
int dist[105][105];
struct node{
    int x, y, w;
    bool friend operator < (node a, node b){
        return a.w > b.w;
    }
};
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};

/*
PS:本來想用純陣列加指標的方式做的。
後來寫著寫著把自己寫死掉了,還是 vector 方便。
普通的二維陣列傳來傳去簡直要我的命。
::: 雖然我的編譯器見不得 c++11 的標準,每次都給我提 warning。頭大。
*/

// 轉換,將每一個怪獸的血量都轉換成對應的數字。
int calc(char c){
    if (c == '.') return 0;
    if (c == 'R') return 1;
    if (c == 'B') return 3;
    if (c == 'D') return 8;
    if (c == 'S') return 24;
    if (c == 'G') return 36;
    return 0x7f7f7f7f;
}

// 最普通的廣度優先搜尋演算法:
// 記錄從 sx, sy 出發,到當前層 grid 的每一個點的最短路徑。
// 其中 block 代表無法走的格子。
vector<vector<int> > bfs(vector<string> &grid, int sx, int sy, string block){
    vector<vector<int> > dist(n, vector<int>(m, 0x7f7f7f7f));
    dist[sx][sy] = 0;
    struct node{
        int x, y;
    }; queue<node> que;
    que.push((node){sx, sy});
    while(!que.empty()){
        node t = que.front();
        que.pop();
        for (int i=0; i<4; i++){
            int cx = t.x + dx[i];
            int cy = t.y + dy[i];
            if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
            // 字串函式,判斷當前格子是否是障礙物,如果是障礙物的話就忽略。
            if (block.find(grid[cx][cy]) != string::npos) continue;
            if (dist[cx][cy] < 0x7f7f7f7f) continue;
            dist[cx][cy] = dist[t.x][t.y] + 1;
            que.push((node){cx, cy});
        }
    }
    return dist;
}

// 將 dist 陣列根據 grid 復原。
// 如果當前位置無法被走到,則將 dist 更新為無窮大。
void cover(vector<vector<int> > &dist, vector<string> &grid, string block){
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            if (block.find(grid[i][j]) != std::string::npos)
                dist[i][j] = 0x7f7f7f7f;
        }
    }
    return ;
}

// dijkstra 最短路演算法。
// 主要作用是計算並更新每個節點到其餘節點的最短距離,並透過廣度優先搜尋(BFS)演算法來實現。
// dist[i][j] 是從一開始設定的起點到達 (i, j) 的累積代價,考慮了路徑上經過的每個格子的代價。
void getMinDist(vector<vector<int> > &dist, vector<string> &grid, string block){
    priority_queue<node> que;
    // 一開始把所有起點都入隊。
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            que.push((node){i, j, dist[i][j]});
        }
    }

    while(!que.empty()){
        node t = que.top();
        que.pop();
        if (dist[t.x][t.y] < t.w) continue;
        for (int i=0; i<4; i++){
            int cx = t.x + dx[i];
            int cy = t.y + dy[i];
            if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
            if (block.find(grid[cx][cy]) != string::npos) continue;
            // 如果已經存在更優解了,就忽略。
            if (dist[cx][cy] <= t.w + 1) continue;
            dist[cx][cy] = t.w + 1;
            que.push((node){cx, cy, t.w + 1});
        }
    }
}

// 計算漢密爾頓路徑
// 即從起點出發走完所有的點最後再回來的路徑最短路。
void hamilton(vector<vector<int> > &dist, vector<string> &grid){
    struct node{
        int x, y;
    }; vector<node> vec;
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            if (grid[i][j] == '*')
                vec.push_back((node){i, j});
        }
    }

    int k = vec.size();
    vector<vector<int> > f(k);
    // f 陣列用於計算每一個關鍵節點(怪獸)之間的最短路。
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        auto toOther = bfs(grid, sx, sy, "#");
        for (int j=0; j<k; j++){
            f[i].push_back(toOther[vec[j].x][vec[j].y]);
        }
    }

    // 對於 Hamilton 路徑不熟悉的,直接去搜洛谷模板題先看一眼。
    // X04-01 的同學應該是學過的(畢竟是我挑的題目)。
    vector<vector<int> > dp(1 << k, vector<int>(k, 0x7f7f7f7f));
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        dp[1 << i][i] = dist[sx][sy];
    }
    for (int i=0; i<(1<<k); i++){
        for (int j=0; j<k; j++){
            if (~i >> j & 1) continue;
            for (int l=0; l<k; l++){
                if (~(i ^ 1 << j) >> l & 1) continue;
                dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][l] + f[l][j]);
            }
        }
    }
    for (int i=0; i<k; i++){
        int sx = vec[i].x;
        int sy = vec[i].y;
        dist[sx][sy] = dp[(1 << k) - 1][i];
    }
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m >> k;
    vector<vector<string> > grids(k, vector<string>(n));
    for (auto &grid : grids){
        for (auto &row : grid){
            cin >>  row;
        }
    }
    int cost = k - 1;
    for (auto &grid : grids){
        for (auto &row : grid){
            for (auto &c : row){
                if (c != '#' && c != '.'){
                    cost += calc(c);
                    c = '*';
                }
            }
        }
    }

    vector<vector<int> > dist = bfs(grids[0], 0, 0, "#*");
    
    /*
        先排除掉某些障礙物和關鍵點,以計算初步的最短路徑。
        再使用 hamilton 函式來處理關鍵點之間的路徑規劃,得到所有關鍵點的最優路徑。
        最後進一步排除空白區域和障礙物,重新計算網格中各點之間的最短路徑,得到最終結果。
    */
    for (auto &grid : grids){
        cover(dist, grid, "#*");
        getMinDist(dist, grid, "#");
        hamilton(dist, grid);
        cover(dist, grid, "#.");
        getMinDist(dist, grid, "#");
    }

    // 計算答案。
    int ans = 0x7f7f7f7f;
    for (int i=0; i<n; i++){
        for (int j=0; j<m; j++){
            ans = min(ans, dist[i][j]);
        }
    }

    cout << ans + cost << endl;
    return 0;
}

相關文章