ICPC2023香港站題解(A D E H I J)

maple276發表於2024-04-19

本場金牌為超低罰時六題,穩拿金牌需要做出第七題。

但是我只會六題,這裡是前六題的題解。

ICPC2023香港站

J:

簽到但不是完全簽到,需要講。

首先每個位置只會走一次,所以讓 \(a_i\) 加一的操作只會在第一次到達某個位置時連續施行。

\(a_i\) 加一再跳轉需要花費一個時間,讓 \(a_i\) 加二再跳轉需要花費兩個時間,你可以理解成先走到 \(i+a_i\) 的位置,再花費 1 的時間往後走一個位置,也可以花費 2 的時間往後走兩個位置。所以我們把每一個位置往下一個位置連花費為1的邊就好了。

但這個連邊是需要一個前提的,我們得能夠先走到 \(i\) 的位置,這些向後連的邊才有意義。比如終點是1,但是 \(a_0=2\),你是不能一步到達的。

解決方案很簡單,不要把0當初始點,而是把 \(a_0\) 當初始點,因為走到終點是至少需要跳這一步的。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,X;
ll ans,a[N];
struct E{
    ll to,we;
};
vector <E> e[N];
struct D{
    ll id,di;
};
bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;
ll dis[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

inline void add(ll u,ll v,ll z) {
    e[u].push_back({v,z});
}

void DIJ() {
    memset(dis,0x3f,sizeof(dis));
    q.push({a[0],0}); dis[a[0]] = 0;
    while(!q.empty()) {
        D now = q.top(); q.pop();
        ll u = now.id;
        if(dis[u]!=now.di) continue;
        FOR() {
            ll v = e[u][i].to, w = e[u][i].we;
            if(dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w;
                q.push({v,dis[v]});
            }
        }
    }
}

int main() {
	n = read(); X = read();
    for(ll i=0;i<n;i++) a[i] = read(), add(i,(a[i]+i)%n,1);
    for(ll i=0;i<n;i++) add(i,(i+1)%n,1);
    DIJ();
    cout<<dis[X]+1;
    return 0;
}

A:

一道很像網路流的矩陣選數題。題面中的 NP-complete 把我隊友給誤導了,其實解法很簡單。

和之前一道類似的矩陣構造題很像,先全部改成一致顏色,然後只需要考慮一個方向。

這題的解法也是類似,先把所有的1改成0,之後修改任意位置其實都是加一。為了同時滿足行和列的要求,有一個貪心的選數方法,比較經典。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const int N=501010;
const int qwq=303030;
const int inf=0x3f3f3f3f;

int T;
int n,m;
int a[4040][4040],b[4040][4040];
char s[N];
int hang[N];
struct E{
    int id,zhi;
}lie[N];
inline bool cmp(E A,E B) { return A.zhi < B.zhi; }

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int main() {
	n = read();
    for(int i=1;i<=n;i++) lie[i].id = i;
    for(int i=1;i<=n;i++) {
        scanf("%s",s+1);
        for(int j=1;j<=n;j++) {
            if(s[j]=='-') a[i][j] = -1;
            else {
                a[i][j] = 1;
                hang[i]--;
                lie[j].zhi--;
            }
        }
    }
    for(int i=1;i<=n;i++) hang[i] += read();
    for(int i=1;i<=n;i++) lie[i].zhi += read();
    for(int i=1;i<=n;i++) {
        if(hang[i]>0) { cout<<"No"; return 0; }
        sort(lie+1,lie+n+1,cmp);
        for(int j=1;j<=(-hang[i]);j++) lie[j].zhi++, b[i][lie[j].id] = 1;
    }
    // bool ke = 1;
    for(int i=1;i<=n;i++) if(lie[i].zhi!=0) { cout<<"No"; return 0; }
    cout<<"Yes\n";
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=n;j++) {
            if(a[i][j]==1) {
                if(b[i][j]) cout<<0;
                else cout<<1;
            }
            else {
                if(b[i][j]) cout<<1;
                else cout<<0;
            }
        }
        cout<<endl;
    }
    return 0;
}

I:

當你的重置能力CD好了之後,你有兩個選擇:一是等你的普通攻擊CD,CD好了之後先攻擊,再重置,然後立即攻擊;二是馬上重置,然後攻擊。

其他的選擇,你手玩一下會發現是愚昧的。

而這兩個選擇結果都是相同的,都會使得兩個技能重新開始冷卻(其實就和重新開始一個情況了)。

這兩種選擇,週期不一樣,攻擊次數不一樣。

資料範圍允許我們列舉第二種選擇的數量,然後推算出第一種選擇的數量,剩餘部分一直平A。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,m;
ll A,B;

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int main() {
	T = read();
    while(T--) {
        A = read(); B = read(); m = read();
        ll k = B/A;
        if(B%A==0) {
            cout<<(m/A+m/B+2)*160<<"\n";
            continue;
        }
        k++;
        ll res = 0;
        for(ll i=0;i<=m;i+=B) {
            ll wo = k*(i/B);
            ll sheng = m-i;
            ll ans = wo + sheng/(k*A) + sheng/A + 2;
            res = max(res,ans);
        }
        cout<<res*160<<"\n";
    }
    return 0;
}

D:

每個點入度上限為3,簡單多了。

首先一個點不能連三個紅邊,也不能連三個藍邊,所以紅邊的連通塊只能是鏈或環。

然後發現連成環也允許,一個紅色環至少需要三個點,這些點無法透過藍邊連起來。

所以藍色連通塊和紅色連通塊都是鏈。

然後發現,鏈的長度還不能超過四,也就是說最多四個點,因為一個點想要成為紅點的鏈中意味著它要連兩個紅邊,只能連一個藍邊,它就是藍邊的鏈頭或鏈尾,我們不允許三個鏈頭鏈尾出現,所以紅鏈只能有兩個鏈中,鏈長度最多是四。

然後就是分類討論,一個點,兩個點,三個點,四個點。四個情況。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,m;
ll ans;
vector <ll> e[N],d[N];
map <ll,ll> f,g;
ll si[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

ll check(ll A,ll C) {
    if(!A || !C) return 0;
    return (f[A*n+C] && g[A*n+C]);
}

int main() {
    ll x,y,z;
    n = read(); m = read();
    for(ll i=1;i<=m;i++) {
        x = read(); y = read(); z = read();
        if(z==1) {
            e[x].push_back(y);
            e[y].push_back(x);
            f[x*n+y] = f[y*n+x] = 1;
        }
        else {
            d[x].push_back(y);
            d[y].push_back(x);
            g[x*n+y] = g[y*n+x] = 1;
        }
    }
    for(ll i=1;i<=n;i++) if(e[i].size()==3 || d[i].size()==3) si[i] = 1;
    ans = n;
    ll res = 0;
    for(auto v : f) { if(g[v.first]) res++; }
    ans += res / 2;

    res = 0;
    ll A, B, C, D;
    for(ll i=1;i<=n;i++) {
        if(si[i]) continue;
        A = B = C = D = 0;
        if(e[i].size()==2) A = e[i][0], B = e[i][1];
        if(e[i].size()==1) A = e[i][0];
        if(d[i].size()==2) C = d[i][0], D = d[i][1];
        if(d[i].size()==1) C = d[i][0];
        res += check(A,C) + check(B,C) + check(A,D) + check(B,D);
    }
    ans += res;

    res = 0;
    for(ll i=1;i<=n;i++) {
        if(e[i].size()!=1) continue;
        A = i; B = e[i][0];
        if(e[B].size()!=2) continue;
        if(e[B][0]==A) C = e[B][1];
        else           C = e[B][0];
        if(e[C].size()!=2) continue;
        if(e[C][0]==B) D = e[C][1];
        else           D = e[C][0];
        if(e[D].size()!=1) continue;
        if(g[A*n+B] && g[A*n+D] && g[C*n+D]) res++;
        if(g[A*n+C] && g[A*n+D] && g[B*n+D]) res++;
    }
    ans += res / 2;
    cout<<ans;
    return 0;
}

H:

妙妙樹上DP,我寫的這個是一個很奇怪的做法。

首先要發現一點:雖然車是按從小到大一個一個尋找空位的,但我們不需要關心它們的具體順序,只要是一個合法的方案,我們調換任意兩輛車的順序,依舊是合法的。

所以我們dp時就不需要考慮車的編號了,只需要考慮每個點上有幾輛車。

最後的答案如何統計,假如第 \(i\) 個點停了 \(b_i\) 輛車,那麼這樣的方案對應到車輛編號的情況數就有 \(\frac{n!}{b_1!b_2!...b_n!}\) 種,也就是超排列。我們不能讓最後的方案數直接乘以 \(n!\) 就是因為有些車是在同一個點的,直接乘以 \(n!\) 會重複計算這一部分。我們 dp 出來的方案數不好統計究竟有多少車在同一個點,但我們發現可以把 \(b_i!\) 分母的這部分先計入我們的dp,也就是說,只要選了一個大小為 \(b_i\) 的點(\(b_i\) 輛車在這個點),我們就讓 dp 結果除以 \(b_i\),最後再乘上一個 \(n!\) 就是最終答案了。

dp 方程怎麼設,怎麼轉移,需要觀察題目的性質。

因為題目中的過程所有車都是根向走的,我們將時間倒流,每個點上有一輛車,所有車往葉向走回到初始時刻,你會發現一個性質,每一棵子樹中車的數量一定不小於子樹大小,而且多出來的部分也不會超過這棵子樹到根的距離。題目所給的 “隨機資料” 的性質就在這裡體現了,每個點到根的距離期望值不會超過log。

我們設 \(f[u][i]\) 表示 u 這棵子樹中車比點多了 i 個的方案數,兒子們多出來的點數加起來就是父親多出來的點數,所以這是一個累加求和的揹包問題。合併完之後我們要選擇 u 這個父親結點上車的數量,這時我們要讓答案除以 \(b_u\),具體含義就是上一段所講的內容。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=998244353;

ll T;
ll n,m;
vector <ll> e[N];
ll dep[N];
ll f[N][123],g[123];

ll F[N],ni[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

inline ll ksm(ll aa,ll bb) {
    ll sum = 1;
    while(bb) {
        if(bb&1) sum = sum * aa %p;
        bb >>= 1; aa = aa * aa %p;
    }
    return sum;
}

void qiu() {
    F[0] = ni[0] = 1;
    for(ll i=1;i<=N-10;i++) F[i] = F[i-1] * i %p;
    ni[N-10] = ksm(F[N-10],p-2);
    for(ll i=N-11;i>=1;i--) ni[i] = ni[i+1] * (i+1) %p;
}

void DFS(ll u) {
    FOR() {
        ll v = e[u][i];
        dep[v] = dep[u] + 1;
        DFS(v);
    }
}

void TREE(ll u) {
    for(ll v : e[u]) TREE(v);
    f[u][0] = 1;
    ll now = 0;
    for(ll v : e[u]) {
        memset(g,0,sizeof(g));
        for(ll j=0;j<=dep[v];j++) {
            for(ll k=0;k<=now;k++) {
                if(j+k>dep[u]+1) break;
                (g[j+k] += f[u][k] * f[v][j] %p) %= p;
            }
        }
        now += dep[v];
        for(ll j=0;j<=min(now,dep[u]+1);j++) f[u][j] = g[j];
    }
    memset(g,0,sizeof(g));
    for(ll i=0;i<=dep[u]+1;i++) {
        ll duo = i-1;
        for(ll j=0;j<=now;j++) if(duo+j>=0 && duo+j<=dep[u]) (g[duo+j] += f[u][j] * ni[i] %p) %= p;
    }
    for(ll i=0;i<=dep[u];i++) f[u][i] = g[i];
}

int main() {
    int x;
    qiu();
	n = read();
    for(ll i=2;i<=n;i++) {
        x = read();
        e[x].push_back(i);
    }
    DFS(1);
    TREE(1);
    cout<<(f[1][0]*F[n])%p;
    return 0;
}

E:

這題我們捏了個很有趣的東西哈哈,我們稱之為 “左偏笛卡爾樹”

我們的思路是醬紫的:

首先看字典序最小的拓撲排序,我們找到最大的那個數字,連一條邊讓它指向右邊的整體,表示原圖中肯定是先訪問了這個點才能訪問右邊的那些點,否則這個數字就不會出現在這裡。

而它左邊的哪些數字呢,我們把它們視作並列的兄弟關係,因為即使它們之間相互不連邊,在最小拓撲排序中數大的點依舊是後訪問。

因此我們可以遞迴地構建一棵樹:找到區間中最大的數,左邊的整體成為它的兄弟,右邊的整體成為它的兒子。

左右區間各找最大點成為兒子,這樣構造的樹是標準的笛卡爾樹,而我們這個是右邊成為兒子,左邊成為兄弟,我們形象地稱之為 “左偏笛卡爾樹”

然後是最大的拓撲排序,原理是一樣的,我們繼續構造一棵樹,把兩棵樹的所有邊加在一起,就是答案的圖。(這裡要注意第二棵樹的邊往第一棵樹里加時,不允許出現右邊的點連向左邊這種情況,可以用第一棵樹的dfn序來判斷,若有這種邊直接不合法)

正確性怎麼證明呢?

首先是必要性:想要得到題目所給的最小最大拓撲序,我們必須存在這些邊。因為我們建圖就是為了滿足這樣的性質,有這些邊才能導致這樣的順序。

然後是充分性:只要我們有了這兩棵樹的所有邊,我們就能得到題目所給的最小最大拓撲序。因為在任意一棵樹種不存在右側連向左側的邊,且兄弟結點從左到右依次增大,我們總是會先訪問左側的點,然後是左側點的兒子(它們比父親結點優先順序更高,因而肯定也要比父親的右兄弟優先順序高),進而訪問右側點,因此一定會得到題目所給的順序。

證畢。

程式碼實現:遞迴造樹,RMQ或線段樹查詢區間最大最小值。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls now<<1
#define rs now<<1|1

using namespace std;
const int N=801010;
const int qwq=303030;
const int inf=0x3f3f3f3f;

int T;
int n,m;
int a[N],b[N];
struct E{
    int mx,mi,idx,idi;
}t[N<<2],ling;
vector <int> e[N];
int du[N];
int vis[N];
int st1[N],st2[N],cnt;
int dfn[N],tim;

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

void add(int u,int v) {
    e[u].push_back(v);
    du[v]++;
}

E pushup(E A,E B) {
    E C = ling;
    if(A.mx > B.mx) C.mx = A.mx, C.idx = A.idx;
    else            C.mx = B.mx, C.idx = B.idx;
    if(A.mi < B.mi) C.mi = A.mi, C.idi = A.idi;
    else            C.mi = B.mi, C.idi = B.idi;
    return C;
}

void built(int now,int l,int r) {
    if(l==r) { t[now] = {a[l],a[l],l,l}; return ; }
    int mid = l+r >> 1;
    built(ls, l, mid);
    built(rs, mid+1, r);
    t[now] = pushup(t[ls],t[rs]);
}

E query(int now,int l,int r,int x,int y) {
    if(x<=l && r<=y) return t[now];
    E res = ling;
    int mid = l+r >> 1;
    if(x<=mid) res = pushup( query(ls, l, mid, x, y), res );
    if(y>mid)  res = pushup( query(rs, mid+1, r, x, y), res );
    return res;
}

void solve(int fa,int l,int r,int cl) {
    if(l>r) return ;
    E wo = query(1, 1, n, l, r);
    if(cl==1) {
        add(fa,wo.mx);
        solve(wo.mx, wo.idx+1, r, cl);
        solve(fa, l, wo.idx-1, cl);
    }
    else {
        if(dfn[wo.mi]<dfn[fa]) { cout<<"No\n"; exit(0); }
        add(fa,wo.mi);
        solve(wo.mi, wo.idi+1, r, cl);
        solve(fa, l, wo.idi-1, cl);
    }
}

void DFS(int u) {
    vis[u] = 1;
    FOR() {
        int v = e[u][i];
        if(vis[v]) continue;
        du[v]--;
        if(!du[v]) DFS(v);
    }
}

void TREE(int u) {
    dfn[u] = ++tim;
    for(int i=e[u].size()-1;i>=0;i--) {
        TREE(e[u][i]);
    }
}

int main() {
    ling = {-inf,inf,0,0};
	n = read();
    for(int i=1;i<=n;i++) {
        a[i] = read();
    }
    built(1, 1, n);
    solve(0, 1, n, 1);
    TREE(0);
    for(int i=1;i<=n;i++) {
        a[i] = read();
    }
    built(1, 1, n);
    solve(0, 1, n, 2);
    for(int i=0;i<=n;i++) {
        if(!du[i] && !vis[i]) DFS(i);
    }
    for(int i=1;i<=n;i++) if(!vis[i]) {cout<<"No\n"; return 0;}
    for(int i=1;i<=n;i++) {
        for(int v : e[i]) {
            st1[++cnt] = i; st2[cnt] = v;
        }
    }
    cout<<"Yes\n";
    cout<<cnt<<"\n";
    for(int i=1;i<=cnt;i++) cout<<st1[i]<<" "<<st2[i]<<endl;
    return 0;
}

相關文章