ICPC2023杭州站題解(B D E F G H J M)

maple276發表於2024-04-17

本場金牌數量較其他場多(45枚),但金牌線題數不多。

五題為分水嶺,五道簡單題過後所有題均為金牌題,其中有四道可做,即A B E F,做出任意一道即可拿金牌。

這裡提供除 A 題以外的所有可做題的題解。

ICPC2023杭州站:

M:

加入比當前選擇的所有數大的數一定會讓平均值上升,因此答案數列中,V圖中的某一側一定會全部被選擇。

#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 <complex>
#include <queue>
#include <map>

using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;

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 T;
int n;
ll a[N],sum[N];

int main() {
    T = read();
    while(T--) {
        n = read();
        int mi = inf, id = 0;
        for(int i=1;i<=n;i++) {
            a[i] = read();
            sum[i] = sum[i-1] + a[i];
            if(a[i]<mi) mi = a[i], id = i;
        }
        db ans = 0.0;
        for(int i=1;i<=id-1;i++) ans = max(ans,(db)(sum[n]-sum[i-1])/(db)(n-i+1));
        for(int i=id+1;i<=n;i++) ans = max(ans,(db)(sum[i])/(db)(i));
        printf("%.15Lf\n",ans);
    }
    return 0;
}

J:

互動題,探索一個圖是菊花圖還是鏈。

手玩起來還是挺容易的,不講了。

#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 <complex>
#include <queue>
#include <map>

using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;

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 T;
int n;
ll a[N],sum[N];

inline void outing(int cl) {
    if(cl) cout<<"! "<<1<<endl;
    else   cout<<"! "<<2<<endl;
    fflush(stdout);
}

inline void solve(int id) {
    int wo = id+2, ni = id+3, id2 = id+1;
    if(wo>n) wo -= n;
    if(ni>n) ni -= n;
    if(id2>n) id2 -= n;
    cout<<"? "<<id<<" "<<wo<<"\n";
    fflush(stdout);
    if(read()) {
        cout<<"? "<<id<<" "<<ni<<"\n";
        fflush(stdout);
        if(read()) outing(0);
        else outing(1);
    }
    else {
        int ci = 0;
        cout<<"? "<<id2<<" "<<ni<<"\n";
        fflush(stdout);
        ci += read();
        cout<<"? "<<id2<<" "<<wo<<"\n";
        fflush(stdout);
        ci += read();
        if(ci==2) outing(0);
        else outing(1);
    }
}

int main() {
    T = read();
    while(T--) {
        n = read();
        int id = 0, num = 0;
        for(int i=1;i<n;i+=2) {
            cout<<"? "<<i<<" "<<i+1<<"\n";
            fflush(stdout);
            if(read()) {
                id = i;
                num++;
            }
        }
        if(num>=2) { outing(1); continue; }
        if(n&1) {
            if(id) solve(id);
            else {
                cout<<"? "<<1<<" "<<n<<"\n";
                fflush(stdout);    
                if(read()) solve(n);
                else outing(1);
            }
        }
        else {
            if(!num) outing(1);
            else solve(id);
        }
    }
    return 0;
}

D:

構造一個序列,除了答案不能為0以外沒有任何限制。

樣例比較有吸引性,裡面有很多我們熟悉的楊輝三角數,但想了想發現還是找不到關係,甚至無厘頭。

一個正常的思路是:因為有一個式子是連乘,如果這個值比較大,我們會涉及到質因數分解之類的推導,這是比較麻煩的。因為不能有0,我們使其大部分為1。

我的解法中大部分的數對都是 2,-1 ,最後剩下一項時我們設成x,解一下方程就能得出 x=3-n。

#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 <complex>
#include <queue>
#include <map>

using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;

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 T;
int n;
ll a[N],sum[N];

int main() {
    T = read();
    while(T--) {
        n = read();
        if(n==2) { cout<<"1 -3 -3 1\n"; continue; }
        if(n==3) { cout<<"1 -10 6 6 -10 1\n"; continue; }
        cout<<1<<" ";
        for(int i=1;i<n;i++) cout<<"2 -1 ";
        cout<<3-n<<"\n";
    }
    return 0;
}

G:

除了上下左右四個方向,蛇還可以縮尾巴(這使得蛇可以走到地圖的任意位置),其實就是原地走一步,我們把原地走一步算成第五個方向。

用這五個方向進行BFS,複雜度顯然是有問題的,因為我們允許原地走也就說明我們允許BFS中的某個狀態滿足它的步數不是當前位置的最短路,這樣的話我們佇列中的狀態數會很多,因為我們每個位置都可以停留,狀態平方級上升。

最佳化一下,一個重要的發現就是,我們原地走當且僅當我們被擋住了去路,否則原地踏步是沒有意義的。

對於每個被擋住去路的地方,我們走到那裡的最短時間只有兩個可能的值:1、蛇尾巴離開的時間;2、蛇頭走到那裡的最短路。

於是,我們的BFS狀態中就可以只允許出現最優狀態了,當去路被擋住時,我們會把那個位置蛇尾離開的時間壓入BFS狀態中。這樣的修改使得BFS中時間不是單調的了,我們得把佇列修改為優先佇列,BFS演算法改成Dijkstra。

#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
#define ull unsigned long long
#include <complex>
#include <queue>
#include <map>

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

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 T;
int n,m,K;
char s[N];
int X[qwq],Y[qwq];
int a[N][N];
int dis[N][N];
int ff[] = {1,-1,0,0};
int gg[] = {0,0,1,-1};
struct D{
    int x,y,di;
};
inline bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;

int main() {
    n = read(); m = read(); K = read();
    for(int i=0;i<=n+1;i++) a[i][0] = a[i][m+1] = -1;
    for(int j=0;j<=m+1;j++) a[0][j] = a[n+1][j] = -1;
    for(int i=1;i<=K;i++) X[i] = read(), Y[i] = read();
    for(int i=1;i<=n;i++) {
        scanf("%s",s+1);
        for(int j=1;j<=m;j++) a[i][j] = (s[j]=='.') ? 0 : -1, dis[i][j] = inf;
    }
    for(int i=1;i<=K;i++) {
        int x = X[i], y = Y[i];
        if(i==1) { q.push({x,y,0}); dis[x][y] = 0; }
        a[x][y] = K-i+1;
    }
    while(!q.empty()) {
        D now = q.top(); q.pop();
        int x = now.x, y = now.y, di = now.di;
        if(now.di!=dis[x][y]) continue;
        for(int k=0;k<4;k++) {
            int xx = x + ff[k];
            int yy = y + gg[k];
            if(a[xx][yy]==-1) continue;
            if(a[xx][yy]<=di+1) {
                if(dis[xx][yy] > di+1) {
                    dis[xx][yy] = di+1;
                    q.push({xx,yy,di+1});
                }
            }
            else {
                if(dis[xx][yy] > a[xx][yy]) {
                    dis[xx][yy] = a[xx][yy];
                    q.push({xx,yy,a[xx][yy]});
                }
            }
        }
    }
    ull ans = 0;
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(dis[i][j]!=inf) ans += (ull)dis[i][j] * (ull)dis[i][j];
    cout<<ans;
    return 0;
}

H:

題目給出了一個基環森林,但是每個點訪問順序是隨機的,我想了很久一個環上的順序該如何處理,後來發現沒必要,只要把所有小朋友分成三類就好。

第一類:\(a_i\ge a_{b_i}+w_{b_i}\) ,無論 \(b_i\) 那位小朋友有沒有加糖,我都比 ta 多,我肯定不加糖。

第二類:\(a_{b_i}+w_{b_i} > a_i\ge a_{b_i}\),ta 加我就加,ta 沒加我就不加。

第三類:\(a_{b_i}>a_i\) ,無論 ta 有沒有加,我都比 ta 少,輪到我我就加。

分好這三類之後,我們將基環樹上的點染成這三種顏色。

一個小朋友最終的期望值,取決於 ta 加糖的機率,所以第一類和第三類的答案已經固定了。

一個環上如果全都是第二類點,那麼無論它們順序如何,它們最終都不會加糖。第二類如果直接或間接指向第一類點,那麼它也一定不會加。只有那些直接或間接指向第三類點的第二類點才需要我們去計算答案。可以發現這些結構是以第三類點為根的樹,樹上連的全是第二類點,沒有了環的討論。

樹上的這些第二類點的答案如何計算,推到這裡就比較簡單了。

對於一個隨機的排列中的兩個值 a 和 b,a 在 b 的前面機率是 \(1/2\),說明深度為 1 的那些第二類點,機率就是 \(1/2\) ;對於隨機排列中的三個值 a,b,c,a 在 b 前面,且 b 在 c 前面,機率是 \(1/6\) , 所以深度為 2 的第二類點機率是 \(1/6\)。結論就是,深度為 x 的第二類點,加糖機率為 \(1/(n+1)!\)

#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 <complex>
#include <queue>
#include <map>

using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=1000000007;

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 T;
ll n;
ll a[N],b[N],w[N],cl[N];
ll ans[N];
ll f[N],ni[N];
vector <ll> e[N];

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 chushihua() {
    for(ll i=1;i<=n;i++) e[i].clear();
}

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,ll dep) {
    (ans[u] += w[u] * ni[dep] %p) %= p;
    FOR() {
        ll v = e[u][i];
        if(cl[v]==2) DFS(v,dep+1);
    }
}

int main() {
    qiu();
    T = read();
    while(T--) {
        chushihua();
        n = read();
        for(ll i=1;i<=n;i++) a[i] = ans[i] = read();
        for(ll i=1;i<=n;i++) b[i] = read();
        for(ll i=1;i<=n;i++) w[i] = read();
        for(ll i=1;i<=n;i++) {
            e[b[i]].push_back(i);
            if(a[i]>=a[b[i]]+w[b[i]]) cl[i] = 1;
            else if(a[i]<a[b[i]]) cl[i] = 3;
            else cl[i] = 2;
        }
        for(ll i=1;i<=n;i++) {
            if(cl[i]==3) DFS(i,1);
        }
        for(ll i=1;i<=n;i++) cout<<(ans[i]%p+p)%p<<" \n"[i==n];
    }
    return 0;
}

E:

有趣的字串題,但是資料範圍也太卡人了,所以又沒那麼有趣了。

為了適應這個5e6的範圍,我思考了很多修改,但為了好寫最終還是選擇了map版本,幸好一發過了,實現時有很多寫的比較醜但是很無奈的地方(比如第65行)。

這題的思路是這樣:

因為發現正著推不好推,我們倒著推:第 i+1 個字串對第 i 個字串造成了什麼限制。

如果第 i 個字串比第 i+1 個長,那麼第 i 個字串的前 \(|s_{i+1}|\) 位有哪些字母就被第 i+1 個串確定了,我們稱之為限制。每個限制都有26個值,表示每個字母出現了幾次,容易發現題目所給的輸入其實就是每個字串的初始限制。

倒著推,如果字串長度越來越大,那麼限制會越來越多,即每次加上最長字串的初始限制,之前的限制依舊在,因為每一個字串前面的幾位都要保持一致。

如果第 i 個字串比第 i+1 個短,就涉及到這題的關鍵了,因為上一個字串是下一個的週期,因此我們可以計算出一個對於上一個字串長度為 \(|s_{i+1}|\mod|s_i|\) 的限制,即按週期分塊最後不夠的那一部分,決定了上一個串的前幾位。事實上對於下一個串的所有限制,都會對上一個串產生限制,產生的新限制的長度不會超過串本身長度。

然後到了令人惱火的複雜度分析階段,本來這題已經做完了,但5e6的範圍卻不允許我們暴力記錄每一個串的所有限制(因為要乘log),而是需要有一定的繼承來降低複雜度。

我選用了一個map來記錄當前串的所有限制。

對於上一個字串長於下一個字串的情況,下一個字串的所有限制直接繼承,並加入一個新的限制。

對於上一個字串短於下一個字串的情況,我們只需列舉長度大於短串的限制,計算出新限制插入回map,map可以去重。

第一個情況會向map中加入一個元素,而第二個情況不會增加,複雜度基本取決於map中元素個數。第二個情況如果頻繁出現,map中元素個數不會超過串的長度,因此資料出現連續下降的小串(倒著看)不影響複雜度。如果出現連續上升的字串,那麼每次map中會多出一個元素,但是這種上升最多隻會上升根號次,複雜度大致飽和於 \(\sqrt {5e6}*log*26\),理論可過,實際上也可以過。這個複雜度分析還是挺複雜的,對於更一般的情況可能需要勢能分析,雖然這裡只能給出大致的分析,但不影響做題。

#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 <complex>
#include <queue>
#include <map>

using namespace std;
const ll N=301010;
const ll qwq=5030303;
const ll inf=0x3f3f3f3f;
const ll p=1000000007;

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 T;
ll n;
char s[qwq];
int ans[qwq];
struct D{
    int ch[26],he;
}a[N],ling,st[N];
int cnt;
map <int,D> f;
bool flag = 0;

void chushihua() {
    f.clear();
    for(int i=1;i<=n;i++) a[i] = ling;
    flag = 0;
    cnt = 0;
}

inline bool check(D A,D B) { // A has B
    for(int i=0;i<26;i++) if(A.ch[i] < B.ch[i]) return !(flag=1);
    return 1;
}

int main() {
    T = read();
    while(T--) {
        chushihua();
        n = read();
        for(int i=1;i<=n;i++) {
            scanf("%s",s);
            a[i].he = strlen(s);
            for(int j=0;j<a[i].he;j++) a[i].ch[s[j]-'a']++;
        }
        int lst = 0;
        for(int i=n;i>=1;i--) {
            if(a[i].he > lst) {
                f[ a[i].he ] = a[i];
            }
            else {
                for(auto it=--f.end(); ; it=--f.end()) {
                    if( (*it).first<=a[i].he ) break;

                    D now = (*it).second;
                    int ci = now.he/a[i].he;
                    for(int j=0;j<26;j++) {
                        now.ch[j] -= a[i].ch[j] * ci;
                        if(now.ch[j]<0) flag = 1;
                    }
                    if(flag) break;
                    now.he %= a[i].he;

                    st[++cnt] = now;

                    if( it==f.begin() ) {
                        f.erase(it);
                        break;
                    }
                    f.erase(it);
                }
                st[++cnt] = a[i];
                for(int j=1;j<=cnt;j++) {
                    if(f.find(st[j].he)==f.end()) f[st[j].he] = st[j];
                    else check(f[st[j].he],st[j]);
                }
                cnt = 0;
            }
            if(flag) break;
            lst = a[i].he;
        }
        D now = ling;
        int id = 0;
        for(auto it=f.begin();it!=f.end();it++) {
            check((*it).second,now);
            for(int i=0;i<26;i++)
                for(int j=now.ch[i];j<(*it).second.ch[i];j++) ans[++id] = i;
            now = (*it).second;
        }
        if(flag) {
            cout<<"NO\n";
            continue;
        }
        cout<<"YES\n";
        for(int i=1;i<=id;i++) cout<<(char)(ans[i]+'a');
        cout<<endl;
        for(int i=2;i<=n;i++) {
            for(int j=a[i].he;j>=1;j--) {
                int shang = j % a[i-1].he;
                if(shang==0) shang = a[i-1].he;
                ans[j] = ans[shang];
            }
            for(int j=1;j<=a[i].he;j++) cout<<(char)(ans[j]+'a');
            cout<<endl;
        }
    }
    return 0;
}

F:

求樹上某點k鄰域內的mex。

一個比較有用的結論,也是這題的突破口:如果一個點的k鄰域包含了一棵樹直徑的兩個端點,那麼這個k鄰域一定包含了整個樹,反之亦然。

所以,小於x的所有點構成的樹,如果被這個k鄰域包含了,那麼這個k鄰域一定包含了這棵樹直徑的兩個端點。

我們考慮每一個答案是如何產生的,首先由0號點自己構成一棵樹,如果它在k鄰域內,就加入1號點,生成新樹;不斷地向內加點,直到數值不再連續,或者新生成的這棵樹的直徑端點突破了k鄰域的範圍,由於每個數只出現一次,因此這個突破k鄰域的值就是mex。

從0號點開始增加的每一個值,我們都可以維護新樹直徑的兩個端點。我們可以二分找到第一個突破k鄰域的值,每次的check只需判斷直徑兩個端點是否在k鄰域內即可。

二分一個log,計算任意兩點距離需要求LCA,又一個log,不幸地TLE了,我們只得把LCA改成O(1)的求法,但這題成為金牌題主要是因為思路,而不是這個小小的最佳化。

#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=1101010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll n,Q;
struct D{
    ll zhi,id;
}a[N];
inline bool cmp(D A,D B) { return A.zhi < B.zhi; }
struct E{
    ll to,we;
};
vector <E> e[N];
ll dep[N];
ll f[N][23],rec[N][23];
ll id1[N],id2[N];
ll dfn[N],tot;
ll ob[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;
}

void DFS(int u,int fa) {
    f[++tot][0] = dep[u];
    rec[tot][0] = u;
    dfn[u] = tot;
    FOR() {
        int v = e[u][i].to, w = e[u][i].we;
        if(v==fa) continue;
        dep[v] = dep[u] + w;
        DFS(v,u);
        f[++tot][0] = dep[u];
        rec[tot][0] = u;
    }
}

inline int ask(int l,int r) {
    int k = ob[r-l+1];
    if(f[l][k] < f[r-(1<<k)+1][k]) return rec[l][k];
    else return rec[r-(1<<k)+1][k];
}

inline int LCA(int x,int y) {
    if(dfn[x]>dfn[y]) swap(x,y);
    return ask(dfn[x],dfn[y]);
}

inline ll ju(ll u,ll v) { return dep[u] + dep[v] - 2 * dep[LCA(u,v)]; }

int main() {
    ob[0] = -1; for(int i=1;i<=N-10;i++) ob[i] = ob[i>>1] + 1;
    ll x,y,z;
	n = read(); Q = read();
    for(ll i=1;i<=n;i++) a[i].zhi = read(), a[i].id = i;
    for(ll i=1;i<n;i++) {
        x = read(); y = read(); z = read();
        e[x].push_back({y,z});
        e[y].push_back({x,z});
    }
    sort(a+1,a+n+1,cmp);
    if(a[1].zhi!=0) {
        while(Q--) cout<<"0\n";
        return 0;
    }
    id1[1] = id2[1] = a[1].id;
    DFS(a[1].id,0);

    for(int k=1;k<=22;k++) {
        for(int i=1;i+(1<<k-1)<=tot;i++) {
            if(f[i][k-1] < f[i+(1<<k-1)][k-1])
                f[i][k] = f[i][k-1], rec[i][k] = rec[i][k-1];
            else
                f[i][k] = f[i+(1<<k-1)][k-1], rec[i][k] = rec[i+(1<<k-1)][k-1];
        }
    }

    ll da = 1;
    for(ll i=2;i<=n;i++) {
        if(a[i].zhi!=i-1) break;
        da = i;
        id1[i] = id1[i-1]; id2[i] = id2[i-1];
        ll zhi = ju(id1[i],id2[i]);
        ll ju1 = ju(id1[i],a[i].id);
        ll ju2 = ju(id2[i],a[i].id);
        if(ju1>zhi || ju2>zhi) {
            if(ju1>ju2) id2[i] = a[i].id;
            else        id1[i] = a[i].id;
        }
    }
    while(Q--) {
        x = read(); z = read();
        if(ju(x,id1[da])<=z && ju(x,id2[da])<=z) { cout<<da<<'\n'; continue; }
        if(ju(x,id1[1])>z) { cout<<"0\n"; continue; }
        ll l = 1, r = da-1, mid, ans;
        while(l<=r) {
            mid = l+r >> 1;
            if(ju(x,id1[mid])<=z && ju(x,id2[mid])<=z) ans = mid, l = mid+1;
            else r = mid-1;
        }
        cout<<ans<<endl;
    }
    return 0;
}

B:

多項式科技題。

首先題目輸出答案的誤差值就是把準確答案改為了區間是否有答案,若 [x,3x] 內有答案,我們輸出1.5x即可。分段使複雜度多一個log。

若我們不考慮顏色,只考慮是否存在兩個燈距離差為d,我們可以簡單地進行卷積,將序列倒過來,i 變為 n+1-i,與原序列做卷積,若第n+1+d項有值,說明表示存在距離為d的兩個燈,且數值為情況數。為了統計 [x,3x] 內是否有答案,我們讓 a 序列只記錄 [x,3x] 內的燈,b 序列記錄所有位置的燈,把 a 倒過來和 b 卷積即可。

現在加上顏色:a 序列記錄 [x,3x] 內燈的顏色,其他位置設成0(沒有燈的位置也設成0),b 序列記錄所有位置燈的顏色,如果存在一個 i 滿足 i 位置的顏色和 i+d 位置不一樣,那麼可以得到:\(a_i-b_{i+d}\neq0\) ,即 \((a_i-b_{i+d})^2\neq 0\),將平方展開可以得到三項卷積的形式,一個是 a 序列的平方,一個是 ab 卷積,一個是 b 序列的平方,我們只需要求出這三項的和,判斷 n+1+d 項是否為0。

要注意計算 a 或 b 序列的平方不能把所有位置都加起來,而是要滿足 \(a_i\neq0\)\(b_{i+d}\neq 0\),我們用 a1 和 b1 序列把所有不為 0 的位置設為1,再和平方項做卷積。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=1010101;
const ll inf=0x3f3f3f3f;
const ll p=998244353, g=3, gi=332748118;

ll Len;
ll r[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;
}

inline ll init(ll wo) {
	ll len = 1, L = 0; while(len<wo) len<<=1, L++;
	for(ll i=0;i<len;i++) r[i] = (r[i>>1]>>1) | ((i&1)<<(L-1));
	return len;
}

void NTT(ll *A,ll len,ll cl) {
	for(ll i=0;i<len;i++) if(i<r[i]) swap(A[i],A[r[i]]);
	for(ll k=1;k<len;k<<=1) {
		ll g1 = ksm((cl==1)?g:gi, (p-1)/(k<<1));
		for(ll j=0;j<len;j+=(k<<1)) {
			ll gk = 1;
			for(ll i=0;i<k;i++,gk=gk*g1%p) {
				ll x = A[i+j], y = gk*A[i+j+k] %p;
				A[i+j] = (x+y) %p; A[i+j+k] = (x-y+p) %p;
			}
		}
	}
	if(cl==1) return ;
	ll inv = ksm(len,p-2);
	for(ll i=0;i<len;i++) A[i] = A[i] * inv %p;
}

void PMUL(ll *F,ll *G) {
	ll len = Len;
	NTT(F, len, 1);
	NTT(G, len, 1);
	for(ll i=0;i<len;i++) F[i] = F[i] * G[i] %p;
	NTT(F, len, -1);
}

ll n,Q,m;
ll a1[N],aa[N],a[N],b1[N],bb[N],b[N],c[N];
ll wei[N];
vector <ll> gg[N];
ll ans[N];

int main() {
	ll x,y;
	n = read(); Q = read();
	for(ll i=1;i<=n;i++) {
		x = read(); y = read(); wei[x] = i;
		c[x] = y; m = max(m,x);
	}
	for(ll i=1;i<=Q;i++) {
		x = read();
		gg[x].push_back(i);
	}
	ll L = 1, R = min(2ll,n), zhong = 1;
	while(1) {
		for(ll i=1;i<=m;i++) {
			if(c[i]) {
				b[i] = c[i];
				b1[i] = 1;
				bb[i] = c[i] * c[i] %p;
				if(wei[i]>=L && wei[i]<=R) {
					a[m+1-i] = c[i];
					a1[m+1-i] = 1;
					aa[m+1-i] = c[i] * c[i] %p;
				}
			}
		}
		Len = init(m+m+1);
		PMUL(aa,b1);
		PMUL(a,b);
		PMUL(a1,bb);
		for(ll i=m+1;i<=2*m;i++) {
			if((aa[i]-2*a[i]+a1[i]+2*p)%p) {
				for(ll v : gg[i-m-1]) {
					if(!ans[v]) ans[v] = zhong;
				}
			}
		}
		if(R==n) break;
		L = R+1; zhong = R*3/2; R = min(R*3,n);
		memset(a,0,sizeof(a));
		memset(a1,0,sizeof(a1));
		memset(aa,0,sizeof(aa));
		memset(b,0,sizeof(b));
		memset(b1,0,sizeof(b1));
		memset(bb,0,sizeof(bb));
	}
	for(ll i=1;i<=Q;i++) cout<<ans[i]<<"\n";
	return 0;
}

後記

在比賽的最後五分鐘,電子科技大學 “UESTC_忒休斯之船” 隊伍在第八發透過了 A 題,進入6題區,但由於罰時過高排在了六題倒數第二。

然而戲劇性的一幕發生了,滾榜過後,金牌線卡在了六題的倒一和倒二之間,我們學校這支隊伍拿到了最後一場區域賽的最後一塊金牌(之前合肥站的最後一塊金牌也是我們學校拿了)。更有意思的是,比較悲慘的六題區唯一個沒有拿到金牌的隊伍,隊名叫 “中山大學_教練我想拿金牌”。

這個賽季結束後,忒休斯之船隊的兩個學長退役,留下一個23級的大一選手,和我們隊重組了隊。

相關文章