2024ccpc濟南邀請賽

AdviseDY發表於2024-11-15

2024CCPC 全國邀請賽(山東)暨山東省賽 題解 更新至 10 題

目錄
  • 2024CCPC 全國邀請賽(山東)暨山東省賽 題解 更新至 10 題
    • Preface
      • 所有程式碼前面的火車頭
    • Problem A. 印表機
    • Problem C. 多彩的線段 2
    • Problem D. 王國英雄
    • Problem E. 感測器
    • Problem F. 分割序列
    • Problem H. 阻止城堡
    • Problem I. 左移
    • Problem J. 多彩的生成樹
    • Problem K. 矩陣
    • Problem L. 路徑的交
    • Problem M. 迴文多邊形
    • PostScript

Preface

這場打崩了,前有A題簽到直接寫不出來,後有M區間dp自己唐完了,很典的區間dp自己叭叭的半天,結果竟然寫不出來,沒有反應過來可以在區間dp的時候就可以算出來面積,總是想著要找出來點集然後暴力硬算,再加上算錯了時間複雜度,最後直接give up了.只能說是非常的弱智了.

我會在程式碼一些有必要的地方加上註釋,簽到題可能一般就不會寫了.

所有程式碼前面的火車頭

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <unordered_map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(vector<T> tem) { for (auto x : tem) cout << x << ' '; cout << endl; }
void cc(int a) { cout << a << endl; }
void cc(int a, int b) { cout << a << ' ' << b << endl; }
void cc(int a, int b, int c) { cout << a << ' ' << b << ' ' << c << endl; }
void fileRead() {
#ifdef LOCALL
    freopen("D:\\AADVISE\\cppvscode\\CODE\\in.txt", "r", stdin);
    freopen("D:\\AADVISE\\cppvscode\\CODE\\out.txt", "w", stdout);
#endif
}
void kuaidu() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); }
inline int max(int a, int b) { if (a < b) return b; return a; }
inline int min(int a, int b) { if (a < b) return a; return b; }
void cmax(int& a, const int b) { if (b > a) a = b; }
void cmin(int& a, const int b) { if (b < a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;


//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------

Problem A. 印表機

很唐的一集啊,第一發沒有注意中間可能爆\(long long\)導致WA,第二發在改的時候不知道怎麼著把上界改成了\(1e9\)導致WA,但是沒看出來,以為是大小還是不夠的原因.

氣的鼠鼠直接開啟了\(int128\),一邊罵著這狗題一邊寫,中間又因為不熟悉\(i128\)出現的各種問題,調了\(1h\)再過的.中間被隊友帶飛直接過了兩道題.

實際上直接開\(long long\),上界調成\(2e9\)就好了.

思路就是二分時間,看每一臺機器在這\(mid\)時間裡最後造的數量有沒有超過\(k\).

唉,一個一眼簽到卻調了\(1h\)的吃屎選手.


//--------------------------------------------------------------------------------
const int N = 1e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e18;
int n, m, T;
int t[N], L[N], W[N];
int k;
//--------------------------------------------------------------------------------
void out(i128 r) {
    string s = "";
    while (r) {
        s += (r % 10) + '0';
        r /= 10;
    }
    reverse(s.begin(), s.end());
    cout << s << endl;
}
int dfs(i128 mid) {
    i128 sum = 0;
    rep(i, 1, n) {
        i128 ll = i128(i128(W[i]) + i128(i128(t[i]) * i128(L[i])));
        // out(ll);
        sum += i128(i128(mid) / i128(ll) * i128(L[i]));
        if (sum >= k) return 1;

        i128 las = mid - ((mid / ll) * ll);
        if (las >= i128(t[i]) * i128(L[i])) sum += i128(L[i]);
        else {
            sum += i128(las / i128(t[i]));
        }

        if (sum >= k) return 1;
    }
    if (sum >= k) return 1;
    return 0;
}



signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n >> k;
        rep(i, 1, n) {
            cin >> t[i] >> L[i] >> W[i];
        }
        i128 l = 0, r = 1e19 + 2;
        // i128 rr = 1e20;
        // out(rr);
        // cc(r);
        // cout << (1ll << 62) << endl;
        while (l + 1 != r) {
            i128 mid = (l + r) / 2;
            // mid = 1e18;
            if (dfs(mid)) r = mid;
            else l = mid;
            // break;
        }
        // cout << r << endl;
        out(r);
    }
    return 0;
}
/*


*/

Problem C. 多彩的線段 2

這個題有點小搞笑了,上來第一眼直介面胡說把他轉化成圖上的問題,每一個線段看成一個點,然後有交集的線段就互相之間連一條邊,然後相鄰的點不能染一樣的顏色,求所有的方案數.然後就屁都想不出來了,貌似後來搜了一下是圖上的一個有點典的問題,複雜度不小.

只能說唐了,思考了之後無果,然後隊友說好像是可以直接模擬做的,瞬間給我茅塞頓開.只能說簽到題開的小丑了.

所以我們只需要這樣按照左端點排個序,從左到右開始遍歷,然後對於列舉當前的線段,找前面有幾個跟它接觸的,有幾個就 \(k\) 減去幾,這就是它自己對於答案的貢獻數字,最後乘起來就好了.

稍微注意的就是為了快點查詢有幾個接觸的,可以用個堆(裡面按照右端點排序),這樣最後複雜度多了一個 \(log\)

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 998244353;
const int INF = 1e16;
int n, m, T;
PII A[N];
//--------------------------------------------------------------------------------
struct node {
    int y;
    bool operator<(const node& q1) const {
        return q1.y < y;
    }
};


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        int k;
        cin >> n >> k;
        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            A[i] = { a,b };
        }
        int tem = k;
        sort(A + 1, A + n + 1, [&](PII a, PII b) {
            return a.first < b.first;
            });
        priority_queue<node> F;
        int ans = 1;
        rep(i, 1, n) {
            while (!F.empty() and F.top().y < A[i].first) {
                tem++;
                F.pop();
            }
            ans *= tem--;
            ans %= mod;
            F.push({ A[i].second });
        }
        
        cc(ans);
    }
    return 0;
}
/*


*/

Problem D. 王國英雄

首先能夠想到的就是我們一定是先能買多少買多少,再能賣多少賣多少,稱這個為 \(one\) 一個週期,然後我們一直進行這個週期.
先說一個基本的式子:
當前擁有的錢是 \(m\),最多能買的麵粉就是 \(x=m/p (向下取整)\),那麼 \(one\) 之後我們的錢是 \(m+(q-p)*x\),耗費的時間是 \(t=(ax+b+cx+d)\)

但是如果我們只是這樣做,時間複雜度並不會允許.因為可能 \(a,b,c,d,x\)都會很小,時間複雜度會直接卡成 \(O(t)\) 級別的,

隊友張神提出了一個很好的最佳化,就是其實我們還能夠進一步求出來能夠買 \(x+1\) 需要的時間, 即設購買 \(l\) 輪之後我們可以買 \(x+1\) 個麵粉了,那麼就有 \(m+l*(q-p)*x>=(x+1)*p\) ,即 $l >= (p(x+1)-m)/((q-p)x) $ ,向上取整就是 \(l\) 的取值.

這樣時間複雜度因為 \(x\) 每次列舉都會 $ +1$ (請聯想\(1+2+...+n=t\)這個式子),所以會變成 \(O(\sqrt[2]t)\).

那麼還有要考慮的就是最後一段,當我們之後不能一直買到 \(x+1\) 的麵粉了,剩下的一段時間,我們直接二分處理(懶得想式子)就好了.


//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        int p, a, b, q, c, d, t;
        cin >> p >> a >> b >> q >> c >> d >> m >> t;

        if (m < p) {
            cout << m << endl;
            continue;
        }

        int ci, t1, ll;
        while (1) {
            //ci是上文提到的x
            ci = m / p;
            //t1是買一次的時間
            t1 = (a + c) * ci + b + d;
            //ll是剛才說的那個能夠買x+1個麵粉的輪數,分子上多了一部分分母-1是為了向上取整,例如a/b,如果想要向上取整,就寫(a+b-1)/b
            ll = (p * (ci + 1) - m + (q - p) * ci - 1) / ((q - p) * ci);
            // cmin(ll, t / (t1));
            //t是我們目前還剩餘的時間,如果不夠就break
            if (ll * t1 > t) break;
            t -= ll * t1;
            m += ll * (q - p) * ci;
        }
        //跳出來之後,記得算一下我們能夠買幾次x個麵粉
        m += t / t1 * (q - p) * ci;
        t -= t / t1 * t1;
        //之後二分買麵粉的個數,如果二分的個數在剩餘的時間能夠買完再賣完就l=mid,最後l就是我們最後買的麵粉.
        int l = 0, r = t + 1;
        while (l + 1 != r) {
            int mid = l + r >> 1;
            if (a * mid + b + c * mid + d <= t) l = mid;
            else r = mid;
        }
        m += (q - p) * l;
        cout << m << endl;


    }
    return 0;
}
/*


*/

Problem E. 感測器

非常有意思的一道題,賽時被M單防了,沒有看著題,之後補題想著思路是能不能直接把感測器掛到線段樹上,然後再做操作,但又覺得時間複雜度不太允許,便作罷.結果發現還真是這樣做,但是要加上一些小最佳化.

首先先開一個線段樹,線段樹上的節點維護的資訊有 \(sum (代表紅球的個數之和)\),還有一個 $vector A \(裡面存感測器編號,當前節點有這個編號就代表編號覆蓋了這個區間,且不會向下\)(\(就是說如果\)[1,4]$區間的節點有編號\(1\),那麼\([1,2]\)區間和\([3,4]\)區間就不會有\()\) 另外再設一個 \(val[i]\) 代表第\(i\)個感測器裡有多少個紅球

這樣空間是不會爆的,節點裡的\(vector\)上最多會有 \(mlogm\) 個編號,但是在更新的時候想著每次少一個,如果編號都要更新\(val\)的話,最後時間複雜度會變成 \(O(nmlogm)\) .

但是實際上我們只關心 \(sum\) 是不是1而已,設當前區間長度\(len\),我們只當 $sum==1 $ 或者 \(sum==0\)的時候再更新就好了. 如果\(sum==1\)\(val\) 就減\(len-1\),這樣複雜度裡的 \(n\) 就會變成常數.

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int val[N];
int ans;
//--------------------------------------------------------------------------------
//namespace or struct:
//線段樹板子,裡面改了add函式,又加了一個dfs函式用來維護節點上的感測器
namespace seg {
#define xl x+x
#define xr x+x+1
    const int N = 5e5 + 10; const int LIM = N * (2.1);
    struct node {
        int sum = 1;
        // vector<int> A;
    };
    node F[LIM];
    vector<int> A[LIM];
    node operator+(const node& q1, const node& q2) {
        node q;
        // q.A.clear();
        q.sum = q1.sum + q2.sum;
        return q;
    }
    void apply(int x, int k) {
        F[x].sum += k;
    }

    void init(int x, int l, int r) {
        A[x].clear();
        if (l == r) {
            F[x] = node();
            //記得清空A,這裡WA了兩發
            A[x].clear();
            return;
        }
        int mid = l + r >> 1;
        init(xl, l, mid), init(xr, mid + 1, r);
        F[x] = F[xl] + F[xr];
    }
    void add(int x, int l, int r, int l1, int r1, int k) {
        if (l1 > r1) return;
        if (l1 <= l and r <= r1) {
            apply(x, k);
            if (F[x].sum == 0) {
                //遍歷當前節點的感測器,val[id]都減1,因為區間長度只能是1此時
                for (auto& id : A[x]) {
                    val[id] -= 1;
                    if (val[id] == 0) ans -= id * id;
                    if (val[id] == 1) ans += id * id;

                }
            }
            return;
        }
        int mid = l + r >> 1;
        if (r1 <= mid) add(xl, l, mid, l1, r1, k);
        else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
        else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
        F[x] = F[xl] + F[xr];

        if (F[x].sum == 1) {
            for (auto& id : A[x]) {
                val[id] -= r - l + 1 - 1;
                if (val[id] == 1) ans += id * id;
                if (val[id] == 0) ans -= id * id;
            }
        }
        else if (F[x].sum == 0) {
            for (auto& id : A[x]) {
                val[id] -= 1;
                if (val[id] == 1) ans += id * id;
                if (val[id] == 0) ans -= id * id;
            }
        }

    }
    node qry(int x, int l, int r, int l1, int r1) {
        if (l1 > r1) return node();
        if (l1 <= l and r <= r1) return F[x];
        int mid = l + r >> 1;
        if (r1 <= mid) return qry(xl, l, mid, l1, r1);
        else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
        else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
    }
    //實現上述說的節點裡的vector
    void dfs(int x, int l, int r, int l1, int r1, int& id) {
        if (l1 <= l and r <= r1) {
            A[x].push_back(id);
            return;
        }
        int mid = (l + r) >> 1;
        if (r1 <= mid) dfs(xl, l, mid, l1, r1, id);
        else if (mid < l1) dfs(xr, mid + 1, r, l1, r1, id);
        else {
            dfs(xl, l, mid, l1, mid, id);
            dfs(xr, mid + 1, r, mid + 1, r1, id);
        }
    }
#undef xl
#undef xr
}
//-----------------------

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        seg::init(1, 1, n);
        rep(i, 1, m) {
            int l, r; cin >> l >> r;
            //小球的下標是從0開始的!!!
            l += 1, r += 1;
            val[i] = r - l + 1;
            if (val[i] == 1) ans += i * i;
            seg::dfs(1, 1, n, l, r, i);
        }
        cout << ans << " ";
        rep(i, 1, n) {
            int a; cin >> a;
            a += ans; a %= n; a += 1;
            // cc(a);
            seg::add(1, 1, n, a, a, -1);
            cout << ans << " ";
        }
        cout << endl;

    }
    return 0;
}
/*


*/

Problem F. 分割序列

首先對於這種\(i*s[i]\)的求和,(\(s[i]\)代表區域性內的和,共\(k\)部分),我們可以直接直觀的把他轉化成是字尾和.每一部分的劃分後的貢獻相當於是這一部分的起點 \(l\) 一直到 \(n\) 的求和.這裡可以畫圖理解一下,會更加形象.

所以我們只需要求出來前 \(k-1\) 大的字尾和就好了,劃分 \(k\) 個部分,相當於是切了 \(k-1\) 刀對這個數列.

//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N], suf[N];
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n;

        rep(i, 0, n + 1) suf[i] = 0;

        rep(i, 1, n) {
            cin >> A[i];
        }
        priority_queue<int> F;
        int sum = 0;
        rep2(i, n, 2) {
            sum += A[i];
            suf[i] = suf[i + 1] + A[i];
            F.push({ suf[i] });
        }
        sum += A[1];
        cout << sum << " ";
        while (!F.empty()) {
            auto val = F.top(); F.pop();
            sum += val;
            cout << sum << " ";
        }
        cout << endl;

    }
    return 0;
}
/*


*/

Problem H. 阻止城堡

原諒我是一個\(SB\),我竟然一開始想的只需要在十字路口的時候就放就可以了,碼了半天結果被樣例\(hark\)了.\(ok\),重新整理思路...

這個題寫的就十分\(噁心\)了,能夠半模半覺的感覺化成二分圖或者往網路流那邊靠,但不知道該怎麼寫出來,看了題解才知道怎麼維護,學到了學到了.

我們可以將裡面需要放障礙物的情況分成兩類:一類是x軸上相鄰的,一類是y軸上相鄰的.但是有一種情況就是有時候一個障礙物可以同時阻擋以上兩類.但不能簡單的直接只要是一個十字路口就放一個障礙物,我們可以考慮以下:

\[O \ \ \ \ \ O \]

\[O \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ O \]

\[O \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ O \]

\[O \ \ \ \ \ O \]

我們發現實際只需要兩個障礙物就可以了,放了一個障礙物之後會影響別的地方,這很二分圖的感覺.

平常的二分圖裡面將點分成左右兩部分,然後跑匈牙利.對於這道題我們可以將左右相鄰的點看做左邊的點.上下相鄰的點看做右邊的點,然後如果有十字路口那麼就給這兩個新點連一條邊,建完圖之後跑匈牙利.


//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
bool ff[N];
//--------------------------------------------------------------------------------
//二分圖板子
namespace KM {
    const int N = 5e2 + 10;
    int n, m;
    vector<int> A[N];//n
    int co[N];//m
    bool vis[N];//m
    void init(int n_, int m_) {
        n = n_, m = m_;
        for (int i = 0; i <= max(n, m); i++) {
            A[i].clear();
            co[i] = -1;
        }
    }
    void add(int x, int y) { A[x].push_back(y); }
    bool dfs(int x) {
        for (auto y : A[x]) {
            if (vis[y]) continue; vis[y] = 1;
            if (co[y] == -1 or dfs(co[y])) { co[y] = x; return 1; }
        }
        return 0;
    }
    int work() {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= m; j++) vis[j] = 0;
            if (dfs(i)) cnt++;
        }
        return cnt;
    }
}
struct node {
    int id;
    int q1;
    int q2;
};

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        unordered_map<int, vector<PII>> X, Y;
        cin >> n;

        rep(i, 0, n) ff[i] = 0;

        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            //X軸上的點,1代表是人,0代表是障礙物
            X[a].push_back({ b,1 });
            Y[b].push_back({ a,1 });
        }
        cin >> m;
        rep(i, 1, m) {
            int a, b; cin >> a >> b;
            X[a].push_back({ b,0 });
            Y[b].push_back({ a,0 });
        }
        vector<node> XX, YY;
        //XX是代表二分圖中左邊的點,YY是代表右邊的點
        bool flag = 1;
        for (auto [x, A] : X) {
            sort(A.begin(), A.end(), [&](PII a, PII b) {
                return a.first < b.first;
                });
            PII las = { -INF,0 };
            for (auto [id, fl] : A) {
                //如果las.secong==0 or fl==0:代表同軸上已經有放的障礙物了,就可以直接不管了,不需要放到XX或者YY中
                if (las.first == -INF || (fl == 0 or las.second == 0)) {
                    las.first = id, las.second = fl;
                    continue;
                }
                //判斷有沒有中間挨著的點,有就是說明沒有方案
                if (id == las.first + 1) flag = 0;
                XX.push_back({ x,las.first,id });
                las.first = id, las.second = fl;
            }
        }
        for (auto [y, A] : Y) {
            sort(A.begin(), A.end(), [&](PII a, PII b) {
                return a.first < b.first;
                });
            PII las = { -INF,0 };
            for (auto [id, fl] : A) {
                if (las.first == -INF || (fl == 0 or las.second == 0)) {
                    las.first = id, las.second = fl;
                    continue;
                }
                if (id == las.first + 1) flag = 0;
                YY.push_back({ y,las.first,id });
                las.first = id, las.second = fl;
            }
        }
        // for (auto [a, b, c] : XX) {
        //     cc(a, b, c);
        // }
        // for (auto [a, b, c] : YY) {
        //     cc(a, b, c);
        // }
        if (flag == 0) {
            cout << -1 << endl;
            continue;
        }

        KM::init(XX.size(), YY.size());
        int n1 = XX.size(), m1 = YY.size();
        rep(i, 0, n1 - 1) rep(j, 0, m1 - 1) {
            auto [x, y1, y2] = XX[i];
            auto [y, x1, x2] = YY[j];
            if (x1 < x and x < x2 and y1 < y and y < y2) {
                //滿足十字路口的就加邊
                KM::add(i, j);
                // cc(x, y);
            }
        }
        KM::work();
        using KM::co;
        // rep(i, 0, m1 - 1) cc(i, co[i]);
        // cc(n1);
        vector<PII> ans;
        rep(i, 0, m1 - 1) {
            //代表在這個十字路口放一個障礙物
            if (co[i] != -1) {
                ff[co[i]] = 1;
                ans.push_back({ XX[co[i]].id,YY[i].id });
            }
        }
        rep(i, 0, n1 - 1) {
            if (ff[i]) continue;
            ans.push_back({ XX[i].id,(XX[i].q1 + XX[i].q2) / 2 });
        }
        rep(i, 0, m1 - 1) {
            if (co[i] != -1) continue;
            ans.push_back({ (YY[i].q1 + YY[i].q2) / 2, YY[i].id });
        }
        cout << ans.size() << endl;
        for (auto [a, b] : ans) {
            cout << a << " " << b << endl;
        }
    }
    return 0;
}
/*


*/

Problem I. 左移

一眼純純簽到,只需要把字串複製一遍,然後 \(for\) 迴圈 \(i\) 掃一遍 $ s[i] $和 \(s[i+len]\) (即字串的頭和尾)一不一樣就好了.

//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;

//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        string s; cin >> s;
        if (s.size() == 1 || (s[0] == s[s.size() - 1])) {
            cout << 0 << endl;
            continue;
        }
        int len = s.size();
        s = s + s;
        bool fl = 0;
        rep(i, 0, len - 1) {
            if (s[i] == s[i + len - 1]) {
                cout << i << endl;
                fl = 1;
                break;
            }
        }
        if (!fl) cout << -1 << endl;

    }
    return 0;
}

Problem J. 多彩的生成樹

一眼最小生成樹,但是需要一些詳細的分類討論.

先從小到大排序邊權,然後合併.
並查集裡面有兩個元素,一個是\(fa\),一個是\(fl\).//分別代表並查集內部的\(fa\)和當前並查集內部有沒有合併過.

如果合併的\(fa\)一樣,但是\(fl=0\)(代表沒有合併過),那就是聯通塊的內部合併.
如果\(fl==1\),就可以直接\(return\)了.

以下都是\(fa\)不一樣的情況,如果\(x\)\(y\)\(fl\)都是\(1\),那麼兩個聯通塊之間聯一條邊就好;
如果有一個是\(1\),那麼\(ans\)就加\(沒有內部聯通的聯通塊大小*當前的邊權\).
如果都是0,那麼就是加\((聯通塊大小的和-1)*當前的邊權\).


int ans = 0;
int val;
namespace DSU {
    const int N = 1e3 + 10;
    int A[N];
    struct Info {
        int fa;
        int siz;
        int fl;
    };
    Info dsu[N];
    void init(int n) {
        //TO DO 記得初始化
        rep(i, 0, n) {
            dsu[i].fa = i, dsu[i].siz = 1;
            dsu[i].fl = 0;
        }
    }
    int find(int x) { if (x == dsu[x].fa) return x; return dsu[x].fa = find(dsu[x].fa); }
    void merge(int x, int y) {
        x = find(x), y = find(y);
        if (x == y and dsu[x].fl) return;
        if (x == y) {
            ans += (A[x] - 1) * val;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 1 and dsu[y].fl == 1) {
            ans += val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 0 and dsu[y].fl == 0) {
            ans += (A[x] + A[y] - 1) * val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[x].fl == 0) {
            ans += (A[x] * val);
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }
        if (dsu[y].fl == 0) {
            ans += A[y] * val;
            dsu[y].fa = x, dsu[x].siz += dsu[y].siz;
            dsu[x].fl = 1;
            return;
        }

    }
    bool same(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return 1; return 0;
    }
    int size(int x) { return dsu[find(x)].siz; }
}
using DSU::dsu;
using DSU::A;
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
struct node {
    int x;
    int y;
    int val;
};
vector<node> ed;
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {
        cin >> n;
        DSU::init(n);
        ans = 0;
        ed.clear();

        rep(i, 1, n) {
            cin >> A[i];
        }
        rep(i, 1, n) {
            rep(j, 1, n) {
                int a; cin >> a;
                ed.push_back({ i,j, a });
            }
        }
        sort(ed.begin(), ed.end(), [&](node& q1, node& q2) {
            return q1.val < q2.val;
            });

        for (auto [x, y, val_] : ed) {
            // cc(x, y, val_);
            val = val_;
            DSU::merge(x, y);
        }
        cout << ans << endl;
    }
    return 0;
}
/*


*/

Problem K. 矩陣

這個就沒什麼特別好講解的了,一共 \(2n\) 個數字,並且只有一個子矩陣是四個角都互不一樣的.

小小構造題,做法有很多.這裡直接說一種做法,貌似也是題解的做法.

直接讓前 \(n-2\) 行從上到下為1,2,3,...,然後最後倒數兩行從左往右是n-1,n,...一直到剩下最後兩列,目前填了 \(2n-4\)個了,然後把剩下四個沒有填的數字放進去就好了.


//--------------------------------------------------------------------------------
const int N = 5e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N][N];
//--------------------------------------------------------------------------------


signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        cin >> n;

        int cnt = 0;
        rep(i, 1, n - 2) {
            cnt++;
            rep(j, 1, n) A[i][j] = cnt;
        }

        rep(j, 1, n - 2) {
            cnt++;
            rep(i, n - 1, n) {
                A[i][j] = cnt;
            }
        }

        A[n][n] = ++cnt, A[n - 1][n] = ++cnt;
        A[n - 1][n - 1] = ++cnt, A[n][n - 1] = ++cnt;

        cout << "Yes" << endl;
        rep(i, 1, n) {
            rep(j, 1, n) {
                cout << A[i][j] << " ";
            }
            cout << endl;
        }

    }
    return 0;
}
/*


*/

Problem L. 路徑的交

前置知識:每一次修改一條邊權 動態維護樹的直徑

這個題沒有補,實在是補不動了.但是大體思路是差不多的.就是最終的路徑如果能被選擇,他的兩段一定都是要大於等於\(k_i\)的.所以我們如果在原本的樹上從葉子節點開始都去掉\(k_i\)個點之後,剩餘的新樹我們跑一下直徑就好了.

以上是\(k\)固定的情況,那\(k\)不固定的時候,我們可以離線處理答案,使\(k\)從小到大,那麼樹就是從外層開始一層一層被刪去的,(刪邊權可以使得邊權為\(0\)),刪去後再修改一條邊權再求樹的直徑,轉化成板子題了.為了這個題專門去補了其前置知識.

不貼程式碼有點難受,貼一個動態維護樹直徑的板子吧...


//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int w;
struct node {
    int x;
    int y;
    int val;
};
vector<node> ed;
vector<PII> A[N];
int dis[N], L[N], R[N], dep[N];
int tot;
int O[N];

//--------------------------------------------------------------------------------
//namespace :
namespace seg {
#define xl x+x
#define xr x+x+1
    //TODO 調整N的大小
    const int N = 2e5 + 10; const int LIM = N * (2.7);
    struct node {
        int ans = 0;
        int mmax = 0;
        int mmin = 0;
        int rm = 0;
        int lm = 0;
        int lan = 0;
    };
    node F[LIM];
    //TODO up函式
    node operator+(const node& q1, const node& q2) {
        node q;
        q.lan = 0;
        q.ans = max({ q1.ans,q2.ans,q1.mmax + q2.lm,q1.rm + q2.mmax });
        q.mmax = max(q1.mmax, q2.mmax);
        q.mmin = min(q1.mmin, q2.mmin);
        q.lm = max({ q1.lm,q2.lm,q2.mmax - 2 * q1.mmin });
        q.rm = max({ q1.rm,q2.rm,q1.mmax - 2 * q2.mmin });
        return q;
    }
    //TODO apply函式
    void apply(int x, int k) {
        F[x].lm -= k;
        F[x].rm -= k;
        F[x].mmax += k;
        F[x].mmin += k;
        F[x].lan += k;
    }

    void init(int x, int l, int r) {
        if (l == r) { F[x] = node(); return; }
        int mid = l + r >> 1;
        init(xl, l, mid), init(xr, mid + 1, r);
        F[x] = F[xl] + F[xr];
    }
    void down(int x) { if (!F[x].lan) return; apply(xl, F[x].lan), apply(xr, F[x].lan); }
    void add(int x, int l, int r, int l1, int r1, int k) {
        if (l1 > r1) return;
        if (l != r) down(x); F[x].lan = 0;
        if (l1 <= l and r <= r1) { apply(x, k); return; }
        int mid = l + r >> 1;
        if (r1 <= mid) add(xl, l, mid, l1, r1, k);
        else if (mid < l1) add(xr, mid + 1, r, l1, r1, k);
        else add(xl, l, mid, l1, mid, k), add(xr, mid + 1, r, mid + 1, r1, k);
        F[x] = F[xl] + F[xr];
    }
    node qry(int x, int l, int r, int l1, int r1) {
        if (l1 > r1) return node();
        if (l != r) down(x); F[x].lan = 0;
        if (l1 <= l and r <= r1)  return F[x];
        int mid = l + r >> 1;
        if (r1 <= mid) return qry(xl, l, mid, l1, r1);
        else if (mid < l1) return qry(xr, mid + 1, r, l1, r1);
        else { return qry(xl, l, mid, l1, mid) + qry(xr, mid + 1, r, mid + 1, r1); }
    }
#undef xl
#undef xr
}
//------------------------------------

void dfs(int x, int pa) {
    dep[x] = dep[pa] + 1;
    L[x] = ++tot;
    O[tot] = x;
    for (auto [y, val] : A[x]) {
        if (y == pa) continue;
        dis[y] = dis[x] + val;
        dfs(y, x);
        O[++tot] = x;
    }
    R[x] = tot;
}

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    //cin >> T;
    while (T--) {
        int q; cin >> n >> q >> w;
        rep(i, 1, n - 1) {
            int a, b, c; cin >> a >> b >> c;
            ed.push_back({ a,b,c });
            A[a].push_back({ b,c });
            A[b].push_back({ a,c });
        }
        dfs(1, 0);
        seg::init(1, 1, tot);
        rep(i, 1, tot) {
            seg::add(1, 1, tot, i, i, dis[O[i]]);
        }
        int las = 0;
        rep(i, 1, q) {
            int d, e;
            cin >> d >> e;
            int dd = (d + las) % (n - 1);
            int ee = (e + las) % w;
            int t;
            if (dep[ed[dd].x] > dep[ed[dd].y]) t = ed[dd].x;
            else t = ed[dd].y;
            // if (i == 3) cc(dd, t, ee - ed[dd].val);
            seg::add(1, 1, tot, L[t], R[t], ee - ed[dd].val);
            las = seg::qry(1, 1, tot, 1, tot).ans;
            cout << las << endl;
            ed[dd].val = ee;
        }
    }
    return 0;
}
/*


*/

Problem M. 迴文多邊形

非常吃屎的一集,看到題手玩一下,發現很像區間\(dp\),但是又覺得不太能\(ok\),鑑定是區間\(dp\)做少了導致的,回去惡補區間地痞.中間想的時候也想到了這個題裡面的好幾個\(trick\),後面看題解的時候一眼就會了,想死.

假設一個陣列是(點代表別的數字)

\[3 \ \ 3 \ .\ .\ .\ \ 3 \ . \ 3 \ . \ . \ 3 \ \ . \ . \ 3 \]

我們如果要從兩端往中間做選擇,逐漸選到最後,形成一個迴文的序.
\(trick1:\)我們不會在不選擇第一個\(3\)和最後一個\(3\)的情況去選擇裡面別的\(3\),如果有這種情況,我們肯定還要在把最外層的\(3\)一對也都選上才對,這樣的面積才是最大的.

\(trick2:\)我們\(dp\)的下標\(l,r\)定義成選擇了\(l,r\)兩個點的最終值,有便於方程轉移和計算.假設當前的兩段是\(l,r\),找到的下一個兩段是\(l_1,r_1\),那麼我們完全可以計算出這之間的面積.把它拆成兩個三角形,利用向量計算就好了.這樣在轉移的時候,我們可以用以上方式算出來,而不是暴力出點集再算面積

\(trick3:\)我們在\(trick1\)的基礎上,要注意,我們可以選擇最左邊的\(3\)和中間的一個\(3\)作為\(l_1和r_1\),這樣算出來的面積也是有可能是最大值.

還有時間複雜度的問題,遞迴的複雜度看不出來可以看轉化成遞推看,\(l,r\)的列舉是\(O(n^2)\),還有端點和中間的匹配還會有一個\(n\),所以最後是\(O(n^3)\)

\(hh\)最傻的地方是我後來按照這種想法想衝一發,但是dfs寫錯了導致時間複雜度搞錯了最後直接開擺.

所以\(dp\)轉移主要圍繞上面幾點來,剩下的詳看程式碼:

//離散化板子
struct LISAN {
    vector<int> F;
    void init(int A[], int n) {
        vector<int>().swap(F);
        rep(i, 1, n) F.push_back(A[i]);
        sort(F.begin(), F.end());
        F.erase(unique(F.begin(), F.end()), F.end());
    }
    void init(vector<int> A) {
        for (auto x : A) F.push_back(x);
        sort(F.begin(), F.end());
        F.erase(unique(F.begin(), F.end()), F.end());
    }
    //找到第一個大於等於val
    int findda(int val) { int x = lower_bound(F.begin(), F.end(), val) - F.begin() + 1; return x; }
    //找到最後一個小於等於val
    int findxi(int val) { int x = upper_bound(F.begin(), F.end(), val) - F.begin(); return x; }
    void change(int A[], int n) {
        rep(i, 1, n) A[i] = findda(A[i]);
    }
};

struct POINT {
    int x;
    int y;
};
//計算面積
int cal(POINT a, POINT b, POINT c) {
    int tem = 0;
    tem = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
    if (tem < 0) tem *= -1;
    return tem;
}
//--------------------------------------------------------------------------------
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int dp[N][N];
int A[N];
//zuo[i][j]陣列代表從i的下標往左,最近的權值=j的下標,you陣列同理
int zuo[N][N], you[N][N];
LISAN ds;
POINT pos[N];
//--------------------------------------------------------------------------------


int dfs(int l, int r) {
    // cc(12312321);
    if (dp[l][r] != -1) return dp[l][r];
    if (l >= r) return dp[l][r] = 0;
    if (A[l] != A[r]) return dp[l][r] = 0;
    if (l + 1 == r) return dp[l][r] = 0;
    // cc(l, r);
    int tem = 0;
    //列舉i,找到距離r-1左邊最近的一樣的值當我們新的端點
    rep(i, l + 1, r) {
        int j = zuo[r - 1][A[i]];
        if (i > j) continue;
        cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
    }
    //與上面同理,列舉j,找到距離l+1右邊最近的一樣的值當新的端點
    //這樣的遍歷方式使得我們不會有上述的選擇中間的3而沒有選擇最兩端的3
    rep2(j, r - 1, l) {
        int i = you[l + 1][A[j]];
        if (i > j) continue;
        cmax(tem, dfs(i, j) + cal(pos[l], pos[r], pos[i]) + cal(pos[i], pos[j], pos[r]));
    }
    // cc(tem);
    dp[l][r] = tem;
    return dp[l][r];
}

signed main() {
    fileRead();
    kuaidu();
    T = 1;
    cin >> T;
    while (T--) {

        cin >> n;


        rep(i, 1, n + n) rep(j, 1, n + n) {
            dp[i][j] = -1;
            zuo[i][j] = 0;
            you[i][j] = 0;

        }

        rep(i, 1, n) {
            cin >> A[i];
            A[n + i] = A[i];
        }
        rep(i, 1, n) {
            int a, b; cin >> a >> b;
            pos[i] = { a,b };
            pos[i + n] = { a,b };
        }

        int mmax = 0;
        //離散化,因為點數不多,但是權值很大.
        ds.init(A, n + n);
        ds.change(A, n + n);
        rep(i, 1, n) cmax(mmax, A[i]);
        // cc(mmax);
        //預處理zuo陣列
        rep(i, 1, n + n) {
            rep(j, 1, mmax) {
                zuo[i][j] = zuo[i - 1][j];
                if (A[i] == j) zuo[i][j] = i;
            }
        }
        //預處理you陣列
        rep2(i, n + n, 1) {
            rep(j, 1, mmax) {
                you[i][j] = you[i + 1][j];
                if (A[i] == j) you[i][j] = i;
            }
        }

        // rep(i, 1, n + n) cout << A[i] << endl;
        //計算ans
        int ans = 0;
        // ans = dfs(1, 3);
        rep(i, 1, n + n) {
            rep(j, i + 1, n + n) {
                if (j - i + 1 > n) break;
                cmax(ans, dfs(i, j));
            }
        }
        cout << ans << endl;
    }
    return 0;
}
/*


*/

PostScript

這場打得稀巴爛,前期吃屎後期坐牢,評價是要狠狠的訓.

相關文章