快轉到主要內容

CSES-2217 Collecting Numbers II

目錄


題目連結:https://cses.fi/problemset/task/2217

題意
#

此為「收集數字(Collecting Numbers)」進階版。
初始條件相同:給定 1 到 \( n \) 的排列陣列,由左至右遞增收集數字,計算所需回合數。

在此基礎上增加 \( m \) 次「交換操作」,每次操作會將指定兩個位置的數字互換。
需要求出:在「每次」交換結束後,總陣列皆需要花費幾回合方能收齊。

思路
#

若每次交換後皆重新線性掃描計算回合數,時間複雜度達 \(\mathcal{O}(N \times M)\),將導致 TLE。
需觀察交換操作的局部影響性質以達成 \(\mathcal{O}(1)\) 的狀態轉移。

交換位置 \( a \) 與 \( b \) 的數字 \( v[a] \) 和 \( v[b] \) 時,其位置的變動只會影響與其「數值相鄰」的相對位置。
也就是說,對回合數會產生增減影響的對象僅包含:\( v[a]-1 \)、\( v[a]+1 \)、\( v[b]-1 \)、\( v[b]+1 \) 及其本身。

因此可透過以下步驟 \(\mathcal{O}(1)\) 更新總回合數:

  1. 盤點受影響數對:將可能因交換而改變貢獻值的相鄰數對列出,共包含 \(\{v[a]-1, v[a]\}\), \(\{v[a], v[a]+1\}\), \(\{v[b]-1, v[b]\}\), \(\{v[b], v[b]+1\}\)。為避免兩數字相差一造成重複計算,使用 std::set 過濾去重。
  2. 扣除交換前的貢獻:在尚未交換前,先確認這些數對是否對目前的總回合數有貢獻(也就是滿足前數值大於後數值的位置關係)。若有,將其從回合數中扣減。
  3. 執行交換:將陣列內位置 \( a \) 與 \( b \) 之元素互換,並同步更新 pos 陣列中的位置資訊。
  4. 加回交換後的貢獻:在交換完成的新狀態下,重新檢視上列數對的貢獻情形,若滿足新貢獻,則在總回合加回一。

每次操作僅處理最多四組相鄰數對的貢獻,總時間複雜度降為 \(\mathcal{O}(N + M)\)。

程式碼
#

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

#define pb push_back
#define fi first
#define se second
#define INF LONG_LONG_MAX/1000
#define WA() cin.tie(0)->sync_with_stdio(0)
#define all(x) (x).begin(), (x).end()
#define int long long
#define PII pair<int, int>

// v[第幾個位置] = 原始的數字數值
// pos[數字數值 (0-based)] = 第幾個位置

signed main() { WA();
    int n, m; cin >> n >> m; 
    vector<int> v(n), pos(n);
    
    // 讀入陣列,並將數字數值減 1 轉成 0-based 以對應 pos 陣列
    for (auto &i : v) cin >> i, pos[--i] = &i-v.data();
    
    int cnt = 1;
    // 預先計算未交換前的初始總回合數
    for (int i = 0; i+1 < n; i++) if (pos[i+1] < pos[i]) cnt++;
        
    while (m--) {
        int a, b; cin >> a >> b; 
        // 將輸入的 1-based 位置轉為 0-based
        a--, b--;
        
        // st 儲存受交換影響的相鄰數對 {小值, 大值}
        // 利用 set 過濾重複的數對
        set<PII> st = {
            {v[a]-1, v[a]}, {v[a], v[a]+1},
            {v[b]-1, v[b]}, {v[b], v[b]+1}
        };
        
        // 交換前,判定數對的合法位置後,若具貢獻則予以扣減
        for (auto &[x, y] : st) 
            if (x >= 0 && x < n && y >= 0 && y < n && pos[y] < pos[x]) 
                cnt--;
                
        // 正式交換彼此所在的記錄位置
        swap(pos[v[a]], pos[v[b]]);
        // 正式交換陣列裡面的數字
        swap(v[a], v[b]);
        
        // 交換後,針對新位置重新評估這些數對的貢獻並加回
        for (auto &[x, y] : st) 
            if (x >= 0 && x < n && y >= 0 && y < n && pos[y] < pos[x]) 
                cnt++;
                
        // 即時印出當下被這兩個數字影響完後的最新總回合數        
        cout << cnt << '\n';
    }
}
Piau 的筆記本
作者
Piau 的筆記本
希望我寫下來的東西能夠長久的記在我的腦中