JOISC2017 題解

DaiRuiChen007發表於2023-12-10

\(\text{By DaiRuiChen 007}\)


Contest Link


A. Cultivation

Problem Link

題目大意

在一個 \(r\times c\) 的網格上有 \(n\) 個格子是黑色的。

每次操作可以把所有黑色格子向上下左右的某個方向擴充套件一格,即把所有黑色格子的左側染成黑色(其他三個方向同理)。

求至少需要幾次操作染黑整個網格。

資料範圍:\(r,c\le 10^9,n\le 300\)

思路分析

演算法一

爆搜,時間複雜度 \(\mathcal O(4^{R+C})\)

演算法二

注意到每個節點在經過若干次操作後一定會擴充套件出一個矩形,可以證明無論如何調換操作順序,最終的矩形形狀不變,因此我們得到了一個重要結論:最終擴充套件出的圖形只和上下左右操作的次數有關係,而和其相對順序無關

因此列舉四種操作分別進行的次數 \(u,d,l,r\) 然後暴力驗證即可,時間複雜度 \(\mathcal O(R^3C^3)\)

演算法三

先考慮 \(R=1\) 的情況,此時把所有黑色色方格所在列排序得到 \(c_1,c_2,\dots,c_m\),那麼我們可以把所有的格子分成三類:

  1. 在最左側黑格外:\(i<c_1\),為了保證這些格子被染色應該有 \(l\ge c_1-1\)
  2. 在兩個黑格之間:\(c_1\le i\le c_m\),為了保證這些格子被染色應該有 \(l+r\ge \max\{c_{i+1}-c_1-1\}\)
  3. 在最右側黑格外:\(c_m<i\),為了保證這些格子被染色應該有 \(r\ge C-c_m\)

綜上,\(l+r\ge\max\{\max\{c_{i+1}-c_i-1\},c_1-1+C-c_m\}\)

因此我們可以在 \(\mathcal O(n\log n)\) 的時間複雜度內解決 \(R=1\) 的情況,瓶頸在於對 \(c_i\) 排序。

回到原問題,我們同樣可以列舉 \(u,d\),然後對於每一行求出哪些點染到了這一行,用上面的方法分別處理出關於 \(l,r\) 的限制關係式,然後最後把所有限制關係式聯立既可,時間複雜度 \(\mathcal O(R^3n\log n)\)

演算法四

觀察到對於每一行,染到這一行的點按 \(r_i\) 排序後是連續的,因此直接預處理出每個區間的答案即可。

進一步觀察發現對於每一對 \((u,d)\),其本質不同的行只有 \(\mathcal O(n)\) 個,這些行對應的區間可以用滑動視窗直接維護。

因此我們把所有 \(r_i-u,r_i+d\) 離散化,然後用滑動視窗即可快速求出答案,時間複雜度 \(\mathcal O(R^2n)\)

注意到如果提前把所有 \(r_i\) 排序,那麼離散化可以看作對兩個有序序列歸併排序,因此可以最佳化掉排序的 \(\mathcal O(n\log n)\) 複雜度。

演算法五

考慮 \(u\gets u-1,d\gets d+1\) 的過程,容易發現我們可以把這個過程看做在無限大的地圖中整個網格向下移動了一行。那麼,對於所有 \(u+d\) 一定的 \((u,d)\),我們可以把列舉 \(u,d\) 的過程看成用一個寬為 \(R\) 的視窗掃整個網格的過程。

因此我們列舉 \(u+d\) 的和 \(S\),然後對 \(r_i,r_i+S\) 離散化,求出每一行對應的 \(l,r,l+r\) 的下界,然後用一個寬為 \(R\) 的視窗在每一行上掃,掃的時候用單調佇列維護區間對 \(l,r,l+r\) 下界限制的最大值,最後統一計算答案即可。

時間複雜度 \(\mathcal O(Rn)\)

演算法六

觀察到很多 \(S\) 其實是是在做無效列舉,只有當 \(S\) 變大能影響每一行的形態時,我們才需要考慮這樣的 \(S\)

可以證明此時要麼 \(u=r_i-1\) 此時第一行多一個 \(i\),要麼 \(d=R-r_i\) 此時第 \(R\) 行多一個 \(i\),要麼 \(S=r_{j}-r_{i}-1\),此時中間 \(i,j\) 相交,容易證明不存在其他取法影響行的形態。

當然我們要計算出 \(S\) 的下界以保證每一行都非空,注意到這樣的 \(S\)\(\mathcal O(n^2)\) 級別的,因此直接列舉並用演算法五的方法計算即可。

時間複雜度 \(\mathcal O(n^3)\),可以透過此題。

程式碼呈現

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=301,INF=1e18;
int R,C,n,ans=INF;
struct Point {
    int r,c;
}    a[MAXN];
struct Info {
    int lo,hi,gap;
    Info(): lo(INF),hi(INF),gap(INF) {}
}    f[MAXN][MAXN];
struct RMQ_Queue {
    int q[MAXN<<1],tim[MAXN<<1],val[MAXN<<1],head,tail,siz;
    RMQ_Queue() {
        head=1,tail=0,siz=0;
        memset(q,0,sizeof(q)),memset(tim,0,sizeof(tim)),memset(val,0,sizeof(val));
    }
    inline void insert(int ti,int v) {
        ++siz,tim[siz]=ti,val[siz]=v;
        while(head<=tail&&val[q[tail]]<=v) --tail;
        q[++tail]=siz;
    }
    inline void erase(int ti) {
        while(head<=tail&&tim[q[head]]<ti) ++head;
    }
    inline int qmax() {
        assert(head<=tail);
        return val[q[head]];
    }
};
RMQ_Queue Lo,Hi,Gap;
unordered_map <int,int> rem;
inline int solve(int len) {
    if(rem.find(len)!=rem.end()) return rem[len];
    vector <int> rows;
    for(int i=1;i<=n;++i) rows.push_back(a[i].r);
    for(int i=1;i<=n;++i) rows.push_back(a[i].r+len+1); //[a[i].r,a[i].r+len+1)
    inplace_merge(rows.begin(),rows.begin()+n,rows.end());
    rows.erase(unique(rows.begin(),rows.end()),rows.end());
    int m=rows.size()-1;
    vector <Info> sec(m);
    for(int i=0,l=1,r=0;i<m;++i) {
        while(l<=n&&a[l].r+len+1<=rows[i]) ++l;
        while(r<n&&a[r+1].r<=rows[i]) ++r;
        assert(l<=r); sec[i]=f[l][r];
    }
    Lo=RMQ_Queue(),Hi=RMQ_Queue(),Gap=RMQ_Queue();
    int ret=INF;
    for(int i=0,p=-1;i<m;++i) {
        if(rows[i]+R-1>=rows[m]) break;
        Lo.erase(rows[i]),Hi.erase(rows[i]),Gap.erase(rows[i]);
        while(p+1<m&&rows[p+1]<=rows[i]+R-1) {
            ++p;
            Lo.insert(rows[p],sec[p].lo);
            Hi.insert(rows[p],sec[p].hi);
            Gap.insert(rows[p],sec[p].gap);
        }
        ret=min(ret,max(Gap.qmax(),Lo.qmax()+Hi.qmax()));
    }
    return rem[len]=ret;
} 
signed main() {
    scanf("%lld%lld%lld",&R,&C,&n);
    for(int i=1;i<=n;++i) scanf("%lld%lld",&a[i].r,&a[i].c);
    sort(a+1,a+n+1,[&](Point u,Point v){ return u.r<v.r; });
    for(int l=1;l<=n;++l) {
        vector <int> cols;
        for(int r=l;r<=n;++r) {
            cols.insert(upper_bound(cols.begin(),cols.end(),a[r].c),a[r].c);
            f[l][r].lo=cols.front()-1,f[l][r].hi=C-cols.back(),f[l][r].gap=0;
            for(int i=1;i<(int)cols.size();++i) f[l][r].gap=max(f[l][r].gap,cols[i]-cols[i-1]-1);
        }
    }
    int lb=(a[1].r-1)+(R-a[n].r);
    for(int i=1;i<n;++i) lb=max(lb,a[i+1].r-a[i].r-1);
    for(int i=1;i<=n;++i) {
        for(int j=1;j<=n;++j) {
            int len=(a[i].r-1)+(R-a[j].r);
            if(len>=lb&&len<ans) ans=min(ans,len+solve(len));
        }
    }
    for(int i=1;i<=n;++i) {
        for(int j=i;j<=n;++j) {
            int len=a[j].r-a[i].r-1;
            if(len>=lb&&len<ans) ans=min(ans,len+solve(len));
        }
    }
    printf("%lld\n",ans);
    return 0;
}

B. Port Facility

Problem Link

題目大意

\(n\) 個物品和兩個棧(後進先出),每個物品可能進入其中某個棧,現已知 \(n\) 個物品的進出棧順序,求有多少種安排物品入棧出棧的方式滿足順序要求。

資料範圍:\(n\le 10^6\)

思路分析

假如我們把每個棧的入棧出棧時間看成一個線段的話,那麼同一個棧中的線段要麼不相交要麼包含,因此不能在同一個棧中的物品可以寫成若干組關於 \((l,r)\) 的二維偏序關係。

假如我們把不能在同一個棧中的物品相連,那麼這就是一個統計 2-SAT 解數的問題,若原圖是二分圖則答案為 \(2^b\)\(b\) 為連通塊個數,否則答案為 \(0\)

注意到題目中的二維偏序限制關係可以用主席樹最佳化建圖,然後用擴域並查集統計答案。

下面簡單講一下如何實現解題目中的 2-SAT:

  • 首先建出主席樹,把每個線段掛到對應的主席樹的葉子節點上,並把這兩個節點設為同色。
  • 對於每個二維偏序限制,從對應線段節點連到主席樹某個區間節點上,並把這兩個節點設為異色。
  • 把所有被連邊的主席樹區間節點取出,將這些節點的子樹全部設為同色。

前兩步並查集就可以直接做,而第三步需要離線出所有節點再 BFS 一遍以保證複雜度。

時間複雜度 \(\mathcal O(n\log n\alpha(n))\),注意實現常數。

程式碼呈現

#include<bits/stdc++.h>
#pragma GCC optimize("Ofast")
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){
    int x=0; char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
    return x;
}
using namespace std;
const int MAXN=1e6+1,MAXV=MAXN*24,MOD=1e9+7;
int n,siz;
struct Node {
    int ls,rs;
}    tree[MAXV];
int tar[MAXN]; 
inline void Append(int id,int u,int l,int r,int src,int &des) {
    tree[des=++siz]=tree[src];
    if(l==r) { tar[id]=des; return ; }
    int mid=(l+r)>>1;
    if(u<=mid) Append(id,u,l,mid,tree[src].ls,tree[des].ls);
    else Append(id,u,mid+1,r,tree[src].rs,tree[des].rs);
}
vector <int> sec[MAXN];
inline void Link(int u,int ul,int ur,int l,int r,int pos) {
    if(ul>ur||!pos) return ;
    if(ul<=l&&r<=ur) { sec[u].push_back(pos); return ; }
    int mid=(l+r)>>1;
    if(ul<=mid) Link(u,ul,ur,l,mid,tree[pos].ls);
    if(mid<ur) Link(u,ul,ur,mid+1,r,tree[pos].rs);
}
struct Interval {
    int l,r;
}    a[MAXN];
int root[MAXN],dsu[MAXV<<1],rnk[MAXV<<1];
inline int find(int x) {
    int u=x,fa;
    while(dsu[u]!=u) u=dsu[u];
    while(x!=u) fa=dsu[x],dsu[x]=u,x=fa;
    return u;
}
inline void merge(int u,int v) {
    u=find(u),v=find(v);
    if(u==v) return ;
    if(rnk[u]<rnk[v]) swap(u,v);
    dsu[v]=u,rnk[u]+=(rnk[u]==rnk[v]);
}
bool vis[MAXV],inq[MAXV<<1];
signed main() {
    siz=n=read();
    vector <int> rp;
    for(int i=1;i<=n;++i) a[i].l=read(),a[i].r=read(),rp.push_back(a[i].r);
    sort(a+1,a+n+1,[&](Interval u,Interval v) { return u.l<v.l; });
    sort(rp.begin(),rp.end());
    for(int i=1;i<=n;++i) {
        int lid=lower_bound(rp.begin(),rp.end(),a[i].l)-rp.begin()+1;
        int rid=lower_bound(rp.begin(),rp.end(),a[i].r)-rp.begin()+1;
        Link(i,lid,rid-1,1,n,root[i-1]);
        Append(i,rid,1,n,root[i-1],root[i]);
    }
    iota(dsu+1,dsu+siz*2+1,1);
    fill(rnk+1,rnk+siz*2+1,1);
    auto equal=[&](int u,int v) {
        merge(u,v),merge(u+siz,v+siz);
        if(find(u)==find(u+siz)||find(v)==find(v+siz)) puts("0"),exit(0);
    };
    auto diff=[&](int u,int v) {
        merge(u,v+siz),merge(u+siz,v);
        if(find(u)==find(u+siz)||find(v)==find(v+siz)) puts("0"),exit(0);
    };
    queue <int> Q;
    for(int i=1;i<=n;++i) {
        diff(tar[i],i);
        for(int u:sec[i]) equal(i,u),Q.push(u),vis[u]=true;
    }
    while(!Q.empty()) {
        int u=Q.front(); Q.pop();
        if(u<=n) continue;
        for(int v:{tree[u].ls,tree[u].rs}) if(v) {
            equal(u,v);
            if(!vis[v]) Q.push(v),vis[v]=true;
        }
    }
    int ans=1;
    for(int i=1,i0,i1;i<=n;++i) {
        i0=find(i),i1=find(i+siz);
        if(i0==i1) {
            puts("0");
            return 0;
        } else if(!inq[i0]&&!inq[i1]) {
            ans=ans*2%MOD;
            inq[i0]=inq[i1]=true;
        }
    }
    printf("%d\n",ans);
    return 0;
}

C. Sparklers

Problem Link

題目大意

數軸上有 \(n\) 個人,其位置分別為 \(x_1,x_2,\dots,x_n\),開始時,\(k\) 號手中的煙花處於剛被點亮狀態,若一個煙花不被點亮的人 \(i\) 和一個煙花被點亮的的人到 \(j\) 達同一位置後,\(i\) 手中的煙花會被點亮

每個點亮的煙花可以持續燃燒 \(T\) 秒,每個人可以以任意非負整數的速度移動,求所有人的最大速度至少是多少才能使每個人至少被點亮一次

資料範圍:\(n\le 10^5\)

思路分析

顯然本題答案具有可二分性,二分一個最大速度 \(v\) 之後可以讓所有人都以 \(v\) 的速度移動。

考慮刻畫點燃煙花棒的過程,顯然某個時刻場上有兩個點燃的煙花一定不優於把後點燃的一個留住,跟著先點燃的那個人跑,直到先點燃的那個人燃燒結束時再傳火,可以證明這樣一定更優。

一個顯然的觀察是:對於任意時刻,點燃過煙花的人一定是一個包含 \(k\) 的區間 \([l,r]\),並且每個人都會向 \([l,r]\) 靠近然後在區間終點依次傳火,因此我們得到判定條件:區間 \(\mathbf{[l,r]}\) 中的所有人都能被點燃煙花的充分非必要條件為 \(\mathbf{(r-l)T\ge\dfrac{x_r-x_l}{2v}}\),事實上,這個條件描述的是保證 \([l,r]\) 區間中最後一個被點燃煙花的人合法。

\(e_i=x_i-2\times v\times T\times i\),根據歸納法,那麼區間 \([l,r]\) 合法當且僅當 \(e_l\ge e_r\) 且區間 \((l,r],[l,r)\) 中有一個合法。

因此我們需要從 \([k,k]\) 區間不斷擴充,直到擴充到 \([1,n]\),考慮某個時刻我們讓 \([l,r]\gets [l-1,r]\),若 \(e_{l-1}<e_l\),那麼我們繼續擴充 \(l\) 一定不劣,因為此時擴充 \(r\to r'\) 的由於 \(e_l>e_{l-1}\ge e_{r'}\),可以在擴充 \(l\) 之前先做。

因此每次我們擴充 \([l,r]\)\([l',r]\) 都會使得 \(e_{l'}\ge e_l\),同理, 每次擴充 \([l,r]\)\([l,r']\) 都會使得 \(e_{r'}\le e_r\),不斷根據這個貪心擴充區間 \([l,r]\),當然最終可能擴充到一個區間 \([l^*,r^*]\) 使得任何 \(l'<l^*\) 都有 \(e_{l'}<e_{l^*}\) 且任何 \(r'>r*\) 都有 \(e_{r'}>e_{r^*}\),此時無論如何都無法繼續擴充。

考慮此時進行時光倒流,我們用類似的方法從區間 \([1,n]\) 開始逆向收縮,容易證明逆向收縮的過程根據剛才的貪心一定可以收縮到 \([l^*,r^*]\)

因此判斷的時候後從 \([k,k]\)\([1,n]\) 兩端分別貪心地擴充和收縮到 \([l^*,r^*]\) 即可。

時間複雜度 \(\mathcal O(n\log V)\)

程式碼呈現

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1,INF=1e9;
int n,k,T,x[MAXN],e[MAXN];
inline bool check(int v) {
    for(int i=1;i<=n;++i) e[i]=x[i]-2*T*v*i;
    if(e[1]<e[n]) return false;
    int lb=k,rb=k;
    for(int i=k-1;i>=1;--i) if(e[i]>=e[lb]) lb=i;
    for(int i=k+1;i<=n;++i) if(e[i]<=e[rb]) rb=i;
    int l=k,r=k;
    while(lb<l||r<rb) {
        bool ok=false;
        int lp=l;
        while(lb<lp&&e[lp-1]>=e[r]) {
            --lp; if(e[lp]>=e[l]) break;
        }
        if(lp<l&&e[lp]>=e[l]) l=lp,ok=true;
        int rp=r;
        while(rp<rb&&e[rp+1]<=e[l]) {
            ++rp; if(e[rp]<=e[r]) break;
        }
        if(rp>r&&e[rp]<=e[r]) r=rp,ok=true;
        if(!ok) return false;
    }
    l=1,r=n;
    while(l<lb||rb<r) {
        bool ok=false;
        int lp=l;
        while(lp<lb&&e[lp+1]>=e[r]) {
            ++lp; if(e[lp]>=e[l]) break;
        }
        if(lp>l&&e[lp]>=e[l]) l=lp,ok=true;
        int rp=r;
        while(rb<rp&&e[rp-1]<=e[l]) {
            --rp; if(e[rp]<=e[r]) break;
        }
        if(rp<r&&e[rp]<=e[r]) r=rp,ok=true;
        if(!ok) return false;
    }
    return true;
}
signed main() {
    scanf("%lld%lld%lld",&n,&k,&T);
    for(int i=1;i<=n;++i) scanf("%lld",&x[i]);
    int l=0,r=INF,res=INF;
    while(l<=r) {
        int mid=(l+r)>>1;
        if(check(mid)) res=mid,r=mid-1;
        else l=mid+1;
    }
    printf("%lld\n",res);
    return 0;
}

D. Arranging Tickets

Problem Link

題目大意

在一個大小為 \(n\) 的環上,有 \(m\) 種區間,第 \(i\) 種為 \([l_i,r_i)\),有 \(c_i\) 個,對於每個區間可以選擇覆蓋環上的 \([l_i,r_i)\) 還是 \([r_i,l_i)\),最小化所有位置中被覆蓋次數最多的位置的覆蓋次數。

資料範圍:\(n\le 2\times 10^5\)

思路分析

演算法一

顯然第一步先二分答案 \(x\),我們把原問題放回到序列上,變成選擇若干個 \(u_i=[l_i,r_i]\) 的區間變成 \([1,l_i)\cup(r_i,n]\) 然後最小化最大達覆蓋次數。注意到如下的觀察:

觀察一:

存在一組最優解使得所有被反轉的區間 \(\mathbf{u_i}\) 的並集不為空。

證明:

考慮存在兩個反轉的區間 \(u_i,u_j\) 使得 \([l_i,r_i]\cap[l_j,r_j]=\varnothing\),此時考慮同時取消 \(u_i\)\(u_j\) 的反轉。

此時 \(u_i,u_j\) 被覆蓋的次數依然是一次,但是 \([1,n]-u_i-u_j\) 的覆蓋次數從 \(2\) 次變成了 \(0\) 次,顯然反轉之後更優,因此若存在兩個交集不為空的被反轉區間,一定可以透過取消反轉調整成一個更優的解。

\(\mathbf C\) 表示所有被選的區間的集合,記 \(a_i\) 表示每個區間原本被覆蓋的次數,取某個 \(p\in \bigcap_{k\in\mathbf C} u_k\)

先考慮 \(i\in[1,p)\),記 \(t_i\) 表示 \(i\) 被多少 \(k\in \mathbf C\) 的區間 \(u_k\) 包含,那麼 \(i\) 實際被覆蓋的次數 \(c_i\)\(a_i-t_i+(t_p-t_i)=a_i-2t_i+t_p\),由於 \(c_i\le x\) 得到 \(t_i\ge \left\lceil\dfrac{a_i+t_p-x}2\right\rceil\),因此每個 \(t_i\) 有一定限制,顯然從 \(1\)\(p-1\) 依次遍歷後貪心選右端點大的區間補齊 \(t_i\) 即可。最後暴力判斷 \((p,n]\) 中的每個數覆蓋次數是否合法即可。

每次判斷的時候列舉 \(p,t_p\) 暴力檢查即可。

時間複雜度 \(\mathcal O(n^2V\log n\log V)\),其中 \(V=\max_{i=1}^n\{a_i\}\)

演算法二

考慮最佳化 \(p\)\(t_p\) 的列舉過程,注意到如下的觀察:

觀察二:

存在一組最優解使得 \(\mathbf{c_p\in\{\max\{c_i\},\max\{c_{i}\}-1\}}\)

證明:

僅考慮 \(i\not\in\bigcap_{k\in\mathbf C} u_k\),若存在某個 \(c_i\ge c_p+2\),考慮取消某個區間的翻轉,此時 \(c_p\) 至少變大 \(1\)\(\max\{c_i\}\) 要麼變成 \(c_p\) 要麼在原來的基礎上減少,因此不斷進行這個操作可以在保證解最優的情況下將 \(\max{c_i}-c_p\) 縮小到 \(0\)\(1\)

此時由於 \(c_p=a_p-t_p\in\{x,x-1\}\),因此我們得到 \(t_p\in\{a_p-x,a_p-x+1\}\),因此我們將 \(t_p\) 的列舉量從 \(\mathcal O(V)\) 減少到了 \(\mathcal O(1)\),檢查答案的時候只需要列舉 \(p\) 即可。

時間複雜度 \(\mathcal O(n^2\log n\log V)\)

演算法三

繼續挖掘 \(p\) 的性質,觀察到:

觀察三:

存在一組最優解使得 \(\mathbf{a_p=\max\{a_i\}}\)

同樣僅考慮 \(i\not\in\bigcap_{k\in\mathbf C} u_k\),此時 \(c_p=a_p-t_p,c_i=a_i+t_p-2t_i\),注意到 \(t_i<t_p\),因此 \(c_i=a_i+t_p-2t_i> a_i-t_p\)

根據觀察二可知 \(c_i\le c_p+1\),因此 \(a_p-t_p+1>a_i-t_p\),所以有 \(a_p\ge a_i\),原命題得證。

因此我們只需要取 \(a_p=\max\{a_i\}\)\(p\) 即可,再用上述的方法確定 \(t_p\) 並求解即可。

時間複雜度 \(\mathcal O(n\log n\log V)\)

程式碼呈現

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+1,INF=1e18;
int a[MAXN],b[MAXN],l[MAXN],r[MAXN],c[MAXN];
struct Info {
    int r,cnt;
    Info(int _r=0,int _c=0): r(_r),cnt(_c) {}
    inline friend bool operator <(const Info &u,const Info &v) {
        return u.r<v.r;
    }
};
vector <Info> I[MAXN];
signed main() {
    int n,m;
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;++i) {
        scanf("%lld%lld%lld",&l[i],&r[i],&c[i]);
        if(l[i]>r[i]) swap(l[i],r[i]);
        a[l[i]]+=c[i],a[r[i]]-=c[i];
    }
    for(int i=1;i<=n;++i) a[i]+=a[i-1];
    int pos=max_element(a+1,a+n+1)-a;
    for(int i=1;i<=m;++i) {
        if(l[i]<=pos&&pos<=r[i]) I[l[i]].push_back(Info(r[i],c[i])); 
    }
    auto check=[&](int lim,int cnt) -> bool {
        memset(b,0,sizeof(b));
        priority_queue <Info> Q;
        int cov=0;
        for(int i=1;i<=n;++i) {
            for(auto u:I[i]) Q.push(u);
            while(a[i]+cnt-2*cov>lim) {
                if(Q.empty()) return false;
                auto u=Q.top(); Q.pop();
                int f=min(u.cnt,(a[i]+cnt-lim+1)/2-cov);
//                2*cov>=a[i]+cnt-lim
                cov+=f,b[u.r]+=f;
                if(u.cnt>f) Q.push(Info(u.r,u.cnt-f));
            }
        }
        for(int i=pos;i<=n;++i) {
            b[i]+=b[i-1];
            if(a[i]-cov+2*b[i]>lim) return false;
        }
        return true;
    };
    int l=0,r=a[pos],res=a[pos]+1;
    while(l<=r) {
        int mid=(l+r)>>1;
        if(check(mid,a[pos]-mid)||check(mid,a[pos]-mid+1)) r=mid-1,res=mid;
        else l=mid+1;
    }
    printf("%lld\n",res);
    return 0;
}

E. Broken Device

Problem Link

題目大意

通訊題:

Anna.cpp:輸入一個非負整數 \(x\),輸出到一個 01 串 \(S\) 中並傳遞給 Bruno.cpp,已知 \(S\) 中第 \(p_1,p_2,\dots,p_k\) 位會在傳遞給 Bruno.cpp 之前被賦值成 \(0\)

Bruno.cpp:輸入被修改後的 01 串 \(S\),求出 \(x\) 的值。

資料範圍:\(x\le 10^{18},|S|=150,k\le 40\)

思路分析

考慮以相鄰兩位儲存資訊,只要 \(i,i+1\) 有一個位被破壞那麼設為 \(00\),剩下的 \(\{01,10,11\}\) 分別對應三進位制下的數碼 \(\{0,1,2\}\),以 \(3\) 進位制的方式傳遞 \(x\)

最壞情況下,我們剩下 \(35\) 組數碼錶示 \(x\),表出數的最大值為 \(3^{35}\approx 5\times 10^{16}\),考慮進一步最佳化:

首先可以透過隨機化一個 \(1\sim|S|\) 的排列來得到相鄰兩位的分組,此時每組數碼可用的機率 \(p\) 約為 \(\binom{|S|-2}{k}\div\binom{|S|}{k}\approx 0.536\),最終傳遞上界的期望為 \(3^{75p}\approx 1.57\times 10^{19}\),在隨機化效果不好時依然有機率無法透過本題。

考慮一個最佳化:在只有第 \(i\) 位被破壞時,依然可以表示 \(01\),同理只有第 \(i+1\) 被破壞時依然可以表示 \(10\),因此這兩種情況也能被利用,此時 \(p=\binom{|S|-2}{k}\div\binom{|S|}{k}+\frac 23\binom{|S|-2}{k-1}\div\binom{|S|}k\approx 0.6021\),標出上界的期望被進一步最佳化到 \(3.51\times 10^{21}\),足夠透過本題。

程式碼呈現

Anna.cpp

#include<bits/stdc++.h>
#include "Annalib.h"
#define ll long long
using namespace std;
mt19937 RndEng(19260827);
void Anna(int N,ll X,int k,int P[]) {
    vector <int> idx(N),mark(N,0);
    for(int i=0;i<k;++i) mark[P[i]]=1;
    iota(idx.begin(),idx.end(),0);
    shuffle(idx.begin(),idx.end(),RndEng);
    for(int i=0;i<N;i+=2) {
        int u=idx[i],v=idx[i+1];
        if(mark[u]&&mark[v]) {
            Set(u,0),Set(v,0);
        } else if(mark[u]) {
            if(X%3==0) X/=3,Set(u,0),Set(v,1);
            else Set(u,0),Set(v,0);
        } else if(mark[v]) {
            if(X%3==1) X/=3,Set(u,1),Set(v,0);
            else Set(u,0),Set(v,0);
        } else {
            int d=X%3; X/=3;
            if(d==0) Set(u,0),Set(v,1);
            if(d==1) Set(u,1),Set(v,0);
            if(d==2) Set(u,1),Set(v,1);
        }
    }
}

Bruno.cpp

#include<bits/stdc++.h>
#include"Brunolib.h"
#define ll long long
using namespace std;
mt19937 RndEng(19260827);
ll Bruno(int N,int A[]) {
    vector <int> idx(N);
    iota(idx.begin(),idx.end(),0);
    shuffle(idx.begin(),idx.end(),RndEng);
    ll X=0;
    for(int i=N-2;i>=0;i-=2) {
        int u=A[idx[i]],v=A[idx[i+1]];
        if(!u&&!v) continue;
        else if(!u) X=X*3+0;
        else if(!v) X=X*3+1;
        else X=X*3+2;
    }
    return X;
}

F. Railway Trip

Problem Link

題目大意

你有一個長度為 \(n\) 的序列 \(h_1,h_2,\dots ,h_n\),由該序列生成一個無項帶權完全圖,\(u,v\) 之間的邊權(\(u<v\))定義為滿足 \(u<i<v\)\(h_i\ge\min(h_u,h_v)\)\(i\) 的個數。

\(q\) 次詢問 \(a\to b\) 的最短路。

資料範圍:\(n,q\le 10^5\)

思路分析

考慮刻畫操作形態:假設 \(u\to v\) 的路徑為 \(p_1,p_2,\dots ,p_k\),那麼 \(p_i\) 一定是單峰的,也就是 \(p_1\le p_2\le\cdots\le p_u\ge p_{u+1}\ge\cdots\ge p_k\),證明可以貪心調整完成,進一步挖掘可以知道 \(p_i\to p_{i+1}\) 中間不存在其他大於 \(\min(h_{p_i},h_{p_{i+1}})\) 的位置,否則插入這個位置一定不劣。

綜上,我們只需要處理每個數向左向右第一個大於自己的數,並且從 \(u,v\) 兩邊分別倍增上跳即可。

考慮如何倍增出 \(l(u,2^k)\)\(r(u,2^k)\),一個經典的觀察是到 \(l(u,2^k)\) 要麼從 \(l(l(u,2^{k-1}),2^{k-1})\) 擴充套件要麼從 \(l(r(u,2^{k-1}),2^{k-1})\) 擴充套件,求 \(r(u,2^k)\) 同理。

因此回答詢問的時候從兩側分別倍增即可。

時間複雜度 \(\mathcal O((n+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
int a[MAXN],stk[MAXN],top,L[MAXN][20],R[MAXN][20];
signed main() {
    int n,k,q;
    scanf("%d%d%d",&n,&k,&q);
    for(int i=1;i<=n;++i) scanf("%d",&a[i]);
    top=0;
    for(int i=1;i<=n;++i) {
        while(top&&a[stk[top]]<a[i]) --top;
        L[i][0]=top?stk[top]:i,stk[++top]=i;
    }
    top=0;
    for(int i=n;i>=1;--i) {
        while(top&&a[stk[top]]<a[i]) --top;
        R[i][0]=top?stk[top]:i,stk[++top]=i;
    }
    for(int k=1;k<20;++k) {
        for(int i=1;i<=n;++i) {
            L[i][k]=min(L[L[i][k-1]][k-1],L[R[i][k-1]][k-1]);
            R[i][k]=max(R[L[i][k-1]][k-1],R[R[i][k-1]][k-1]);
        }
    }
    while(q--) {
        int u,v;
        scanf("%d%d",&u,&v);
        if(u>v) swap(u,v);
        int l=u,r=u,ans=0;
        for(int k=19;k>=0;--k) {
            if(max(R[l][k],R[r][k])<v) {
                ans+=1<<k;
                int a=l,b=r;
                l=min(L[a][k],L[b][k]),r=max(R[a][k],R[b][k]);
            }
        }
        u=r,l=v,r=v;
        for(int k=19;k>=0;--k) {
            if(min(L[l][k],L[r][k])>u) {
                ans+=1<<k;
                int a=l,b=r;
                l=min(L[a][k],L[b][k]),r=max(R[a][k],R[b][k]);
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

G. Long Distance Coach

Problem Link

題目大意

某長途巴士發車時刻為 \(0\),到達終點的時刻為 \(x\)。車上裝有飲水機,乘客和司機可以在車上裝水喝。出發前水箱是空的。途中有 \(n\) 個服務站,依次編號為 \(1\dots n\)。巴士到達服務站 \(i\) 的時間是 \(s_i\)。保證 \(s_1<s_2<\dots<s_n\) 嚴格遞增,在服務站可以給飲水機加水,但是要錢,水價為每升 \(w\) 元。

本次巴士有 \(m\) 名乘客(不含司機),對於所有非負整數 \(k\),乘客 \(j\) 在時刻 \(kt+d_j\) 需要裝 \(1\) 升水,在其他時刻不裝水,司機在時刻 \(kt\) 需要裝 \(1\) 升水,在其他時刻不裝水。如果某一名乘客想裝水時飲水機沒水了,這名乘客會怒而下車,此時需要向這名乘客退 \(c_j\) 元,你需要保證司機每次都能裝到水。

保證不會出現兩人在同一時刻需要裝水的情況,保證在服務站或是到達終點時,不存在司機或乘客需要喝水。求最小化對乘客的賠款和裝水費用之和。

資料範圍:\(n,m\le 2\times 10^5\)

思路分析

注意到每次乘客下車都是 \(d_j\) 連續的一段,且不會越過 \(0\) 轉移(司機不能下車)。

因此這是一個經典的數列分段問題,設 \(dp_i\) 表示前 \(i\) 個人的答案。

  • \(i\) 一直不下車:\(dp_i\gets dp_{i-1}+w\left\lceil\dfrac{x-d_i}t\right\rceil\)
  • \((j,i]\) 下車:$dp_i\gets dp_j+C_i-C_j+(j-i)\times w\times \left\lfloor\dfrac {f_i}t\right\rfloor $,其中 \(C_i\)\(c_i\) 的字首和,\(f_i\) 表示 \(i\) 最早的合法下車時間 \(s_x\),注意這個下車時間要滿足 \(d_i\le s_x\bmod t<d_{i+1}\),否則轉移 \(i+1\) 一直不下車的時候可能會出錯。

注意到第二種轉移是典型的斜率最佳化,決策點為 \((j,dp_j-c_j)\),斜率閾值為 \(w\times \left\lfloor\dfrac {f_i}t\right\rfloor\),但這個東西沒有單調性,只能二分單調棧解決。

時間複雜度 \(\mathcal O(n\log n+m\log m)\)

程式碼呈現

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5,inf=1e18;
struct Passenger { int c,d; } a[MAXN];
int c[MAXN],d[MAXN],s[MAXN],id[MAXN];
int dp[MAXN],f[MAXN],q[MAXN];
signed main() {
      int x,n,m,w,t;
      scanf("%lld%lld%lld%lld%lld",&x,&n,&m,&w,&t);
      for(int i=1;i<=n;++i) scanf("%lld",&s[i]);
      s[++n]=x;
      for(int i=1;i<=m;++i) scanf("%lld%lld",&a[i].d,&a[i].c);
      sort(a+1,a+m+1,[&](Passenger u,Passenger v){ return u.d<v.d; });
      for(int i=1;i<=m;++i) c[i]=c[i-1]+a[i].c,d[i]=a[i].d;
      d[m+1]=t;
      iota(id+1,id+n+1,1);
      sort(id+1,id+n+1,[&](int u,int v) {
            if(s[u]%t==s[v]%t) return s[u]<s[v];
            return s[u]%t<s[v]%t;
      });
      fill(f+1,f+m+1,inf);
      for(int i=1,p=1;i<=m;++i) {
            while(p<=n&&s[id[p]]%t<=d[i]) ++p;
            while(p<=n&&s[id[p]]%t<d[i+1]) f[i]=min(f[i],s[id[p]]/t*w),++p;
      }
      int hd=1,tl=1;
      dp[0]=(x+t-1)/t*w;
      for(int i=1;i<=m;++i) {
            dp[i]=dp[i-1]+(x+t-d[i]-1)/t*w;
            auto Y=[&](int j,int k) { return (dp[k]-c[k])-(dp[j]-c[j]); };
            if(f[i]!=inf) {
                  int res=tl,l=hd,r=tl-1;
                  while(l<=r) {
                        int mid=(l+r)>>1;
                        if(Y(q[mid],q[mid+1])>f[i]*(q[mid+1]-q[mid])) res=mid,r=mid-1;
                        else l=mid+1;
                  }
                  int j=q[res];
                  dp[i]=min(dp[i],dp[j]+c[i]-c[j]+(i-j)*f[i]);
            }
            while(hd<tl&&Y(q[tl-1],q[tl])*(i-q[tl])>=Y(q[tl],i)*(q[tl]-q[tl-1])) --tl;
            q[++tl]=i;
      }
      printf("%lld\n",dp[m]);
      return 0;
}

H. Long Masion

Problem Link

題目大意

數軸上有 \(1\sim n\) 的房間,每個房間裡有若干鑰匙,房間 \(i\) 和房間 \(i+1\) 直接相連,但需要一種特定的鑰匙開門。

經過房間可以獲得該房間所有鑰匙,鑰匙可以重複使用。

\(q\) 次詢問能否從 \(s\) 房間到 \(t\) 房間。

資料範圍:\(n,q\le 5\times 10^5\)

思路分析

注意到每個點 \(u\) 出發能到達的點都是一個區間 \([L_u,R_u]\)

考慮記憶化搜尋,每次判斷 \(L_u\to L_u-1\)\(R_u\to R_u+1\) 是否可行,可以透過處理每個門左側和右側最近的鑰匙解決。

可以證明每個點只會開始擴充 \(\mathcal O(1)\) 次。

時間複雜度 \(\mathcal O(n\log n+q)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+1;
int n,q,L[MAXN],R[MAXN],c[MAXN],lk[MAXN],rk[MAXN];
//lk[i]: i->i-1, min{R[x]} 
//rk[i]: i->i+1, max{L[x]}
bool vis[MAXN];
inline void expand(int x) {
    vis[x]=true;
    auto expandL=[&]() { return 1<L[x]&&lk[L[x]]<=R[x]; };
    auto expandR=[&]() { return R[x]<n&&L[x]<=rk[R[x]]; };
    while(expandL()||expandR()) {
        if(expandL()) {
            int u=L[x]-1;
            if(!vis[u]) expand(u);
            L[x]=min(L[x],L[u]),R[x]=max(R[x],R[u]);
        }
        if(expandR()) {
            int u=R[x]+1;
            if(!vis[u]) expand(u);
            L[x]=min(L[x],L[u]),R[x]=max(R[x],R[u]);
        }
    }
}
vector <int> pos[MAXN];
signed main() {
    scanf("%d",&n);
    iota(L+1,L+n+1,1),iota(R+1,R+n+1,1);
    for(int i=1;i<n;++i) scanf("%d",&c[i]);
    for(int i=1;i<n;++i) pos[i].push_back(0);
    for(int i=1;i<=n;++i) {
        int k;
        scanf("%d",&k);
        while(k--) {
            int id;
            scanf("%d",&id);
            pos[id].push_back(i);
        }
    }
    for(int i=1;i<n;++i) pos[i].push_back(n+1);
    for(int i=1;i<n;++i) rk[i]=*(--upper_bound(pos[c[i]].begin(),pos[c[i]].end(),i));
    for(int i=n;i>1;--i) lk[i]=*lower_bound(pos[c[i-1]].begin(),pos[c[i-1]].end(),i);
    for(int i=1;i<=n;++i) if(!vis[i]) expand(i);
    scanf("%d",&q);
    while(q--) {
        int s,t;
        scanf("%d%d",&s,&t);
        puts(L[s]<=t&&t<=R[s]?"YES":"NO");
    }
    return 0;
} 

I. Natural Park

Problem Link

題目大意

互動器內有一個 \(n\) 個的無向連通圖 \(G=(V,E)\)。每次互動你可以詢問一個 \(V'\subseteq V\) 和兩個點 \(x,y\),互動器會告訴你 \(x,y\)\(V'\) 的匯出子圖中是否連通。

試在 \(45000\) 次詢問之內求出 \(G\)

資料範圍:\(n\le 1400,m\le 1500\),所有點度數 \(\le 7\)

思路分析

先考慮怎麼解決一條知道兩個端點的鏈的情況。

考慮遞迴求解,每次確定某條鏈 \(u\to v\) 上編號最小的端點,二分一個 \(k\),加入所有 \(\le k\) 的點判斷 \(u,v\) 是否聯通,找到最小可能的 \(k\),這個 \(k\) 一定在鏈上,然後遞迴拆成 \(u\to k,k\to v\) 兩條鏈,操作次數 \(n\log n\)

然後考慮一般的情況,考慮增量構造,從 \(G=\{0\}\) 開始,每次加入一個 \(G\) 鄰域中的點 \(u\) 並確定 \(u\to G\) 的邊。

這一步也可以考慮二分,由於 \(G\) 在構造的時候一定是聯通的,因此我們類比找鏈的方式,求 \(u\) 鄰域中最小的點。

但這個東西很難 check,考慮一個簡單的 check 想法,假如我們在 dfs 序上二分,找 dfs 序最小的點,那麼 check 就很方便,每次只要二分一個 \(k\),判斷加入 dfs 序 \(\le k\) 的所有點後,\(u\) 是否與根連通即可。

然後我們刪掉和 \(u\) 相鄰的這個點 \(t\),原樹會被分成若干個連通塊,對於每個連通塊 \(B\),若 \(u\to B\) 有邊就遞迴求解即可。

這裡的操作次數分成兩部分:對於二分部分,每條邊只會被二分一次,操作次數 \(m\log n\),對於判斷 \(u\to B\) 是否有邊,顯然刪掉一個 \(t\) 後分出的連通塊個數 \(\le \mathrm{deg}(t)\),因此這一部分的總次數不超過 \(7m\)

然後考慮怎麼找一個 \(G\) 鄰域裡的點,考慮用找鏈的方法,把所有 \(G\) 裡的點看成一個點,然後任取一個 \(G\) 外的點 \(x\),用第一部分裡的方法去暴力找一條 \(G\to x\) 的鏈,然後把鏈上的每個點依次加入即可,這一部分的總操作次數依然是 \(n\log n\) 點。

總操作次數 \((n+m)\log n+7m\le 42400\),可以透過此題。

程式碼呈現

#include<bits/stdc++.h>
#include "park.h"
using namespace std;
const int MAXN=1401;
int n;
struct Edge {
    int u,v;
    Edge(int _u=0,int _v=0): u(_u),v(_v) {}
};
inline int Query(int u,int v,vector <int> V) {
    static int buf[MAXN];
    fill(buf,buf+n,0);
    for(int i:V) buf[i]=1;
    buf[u]=buf[v]=1;
    return Ask(min(u,v),max(u,v),buf);
}
inline void ReportEdge(int u,int v) { Answer(min(u,v),max(u,v)); }
inline vector<Edge> Solve() {
    vector <int> V;
    vector <Edge> E;
    vector <int> inq(n,0);
    vector <vector<int>> adj(n);
    auto InsertNode=[&](int u) -> void {
        V.push_back(u),inq[u]=1;
    };
    auto LinkEdge=[&](int u,int v) -> void {
        assert(inq[u]&&inq[v]);
        adj[u].push_back(v);
        adj[v].push_back(u);
        E.push_back({u,v});
    };
    InsertNode(0);
    while((int)V.size()<n) {
        vector <int> outq,vis(n,0);
        for(int i=0;i<n;++i) if(!inq[i]) outq.push_back(i);
        int nw=outq.front();
        auto GetChain=[&](auto self,int l,int r) -> vector<int> {
            vector <int> bas=(l==0)?V:vector<int>{};
            if(Query(l,r,bas)) return {};
            int ul=0,ur=n-1;
            while(ul<ur) {
                int mid=(ul+ur)>>1;
                auto check=[&](int x) {
                    vector <int> qry=bas;
                    for(int i=0;i<=x;++i) qry.push_back(i);
                    return Query(l,r,qry);
                };
                if(check(mid)) ur=mid;
                else ul=mid+1;
            }
            auto L=self(self,l,ur),R=self(self,ur,r);
            vector <int> ans;
            for(int i:L) ans.push_back(i);
            ans.push_back(ur);
            for(int i:R) ans.push_back(i);
            return ans;
        };
        vector <int> chain=GetChain(GetChain,0,nw),ans;
        chain.push_back(nw);
        auto FindNeighbors=[&](int u) -> void {
            auto MinLink=[&](auto self,vector <int> B) -> vector<int> {
                vector <int> vis(n,1),dfn;
                for(int i:B) vis[i]=0;
                int rt=B.front();
                auto dfs1=[&](auto self,int u) -> void {
                    dfn.push_back(u),vis[u]=1;
                    for(int v:adj[u]) if(!vis[v]) self(self,v);
                };
                dfs1(dfs1,rt);
                int l=0,r=dfn.size()-1;
                while(l<r) {
                    int mid=(l+r)>>1;
                    auto check=[&](int x) {
                        vector <int> subt;
                        for(int i=0;i<=x;++i) subt.push_back(dfn[i]);
                        return Query(rt,u,subt);
                    };
                    if(check(mid)) r=mid;
                    else l=mid+1;
                }
                vector <int> ans{dfn[r]};
                fill(vis.begin(),vis.end(),1);
                for(int i=0;i<(int)dfn.size();++i) if(i!=r) vis[dfn[i]]=0;
                for(int v:V) {
                    if(vis[v]) continue;
                    vector <int> ver;
                    auto dfs2=[&](auto self,int u) -> void {
                        ver.push_back(u),vis[u]=1;
                        for(int v:adj[u]) if(!vis[v]) self(self,v);
                    };
                    dfs2(dfs2,v);
                    if(Query(ver.front(),u,ver)) {
                        vector <int> tmp=self(self,ver);
                        for(int x:tmp) ans.push_back(x);
                    }
                }
                return ans;
            };
            vector <int> Ne=MinLink(MinLink,V);
            InsertNode(u);
            for(int v:Ne) LinkEdge(u,v);
        };
        for(int i=0;i<(int)chain.size();++i) {
            int u=chain[i];
            FindNeighbors(u);
        }
    }
    return E;
}
void Detect(int T,int N) {
    n=N;
    vector <Edge> G=Solve();
    for(auto e:G) ReportEdge(e.u,e.v);
}

J. Abduction 2

Problem Link

題目大意

給一個 \(n\times m\) 的網格圖,每行每列分別有權值。

你可以以如下方式在網格圖上運動:

  • 起始時任選方向。
  • 當到達一個交點時:
    • 若直走權值大於轉彎權值,那麼直走,如果前方是邊界則結束運動。
    • 否則選一種方向轉向。

\(q\) 次詢問從 \((s,t)\) 出發的最長運動距離。

資料範圍:\(n,m\le 5\times 10^4,q\le 100\)

思路分析

直接記搜 \(dp(i,j,d)\) 表示到達第 \((i,j)\) 行接下來方向為 \(j\) 的最長路,用 ST 錶快速求下一個轉向的位置。

對於每個詢問,把擴充到的點圍成一個矩形,顯然在矩形內部的路徑一定會走到矩形上的並轉向。

而矩形內的點只要 \(\mathcal O(1)\) 步走到矩形邊界上,因此最終態就是矩形面積和為 \(\mathcal O(n^2)\) 級別。

顯然每個矩形進行一次搜尋就會使得長或寬增加至少 \(1\),並且一定是交替增加長和寬的。

因此每個矩形擴充 \(\dfrac n{\sqrt q}\) 步後,矩形周長為 \(\mathcal O\left(\dfrac n{\sqrt q}\right)\) 級別,這一部分總擴充次數為 \(\mathcal O(n\sqrt q)\),接下來每一次擴充都會使得矩形面積和增加至少 \(\mathcal O\left(\dfrac n{\sqrt q}\right)\),擴充次數為 \(\mathcal O(n\sqrt q)\) 級別。

時間複雜度 \(\mathcal O(n\sqrt q\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+1;
struct RMQ {
    int f[MAXN][18];
    inline int bit(int x) { return 1<<x; }
    inline void build(int *a,int n) {
        for(int i=1;i<=n;++i) f[i][0]=a[i];
        for(int k=1;k<18;++k) {
            for(int i=1;i+bit(k-1)<=n;++i) {
                f[i][k]=max(f[i][k-1],f[i+bit(k-1)][k-1]);
            }
        }
    }
    inline int query(int l,int r) {
        int k=__lg(r-l+1);
        return max(f[l][k],f[r-bit(k)+1][k]);
    }
}    R,C;
int n,m,q,a[MAXN],b[MAXN];
map <tuple<int,int,int>,int> dp;
const int dx[]={-1,0,1,0},dy[]={0,-1,0,1};
inline int dfs(int x,int y,int d) {
    //d: {U,L,D,R}
    auto T=make_tuple(x,y,d);
    if(dp.count(T)) return dp[T];
    auto valid=[&](int i,int j) { return 1<=i&&i<=n&&1<=j&&j<=m; };
    if(!valid(x+dx[d],y+dy[d])) return dp[T]=0;
    int l=1,r=max(n,m),res=0;
    auto check=[&](int k) -> bool {
        if(!valid(x+k*dx[d],y+k*dy[d])) return false;
        if(d==0) return R.query(x-k,x-1)<=b[y];
        if(d==1) return C.query(y-k,y-1)<=a[x];
        if(d==2) return R.query(x+1,x+k)<=b[y];
        if(d==3) return C.query(y+1,y+k)<=a[x];
        return 0;
    };
    while(l<=r) {
        int mid=(l+r)>>1;
        if(check(mid)) res=mid,l=mid+1;
        else r=mid-1;
    }
    int nx=x+(res+1)*dx[d],ny=y+(res+1)*dy[d];
    return dp[T]=valid(nx,ny)?max(dfs(nx,ny,d^1),dfs(nx,ny,d^3))+res+1:res;
}
signed main() {
    scanf("%lld%lld%lld",&n,&m,&q);
    for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
    for(int i=1;i<=m;++i) scanf("%lld",&b[i]);
    R.build(a,n),C.build(b,m);
    while(q--) {
        int x,y;
        scanf("%lld%lld",&x,&y);
        printf("%lld\n",max(max(dfs(x,y,0),dfs(x,y,2)),max(dfs(x,y,1),dfs(x,y,3))));
    }
    return 0;
}

K. City

Problem Link

題目大意

通訊題,實現兩個程式:

  • encoder.cpp:讀入一棵 \(n\) 個點的深度不超過 \(19\) 的樹,為每個點 \(u\) 分配一個 \([0,2^{28}-1)\) 之內的編號 \(T_u\)
  • device.cpp\(q\) 次詢問,讀入 \(T_x,T_y\),回答 \(x\)\(y\) 在樹上的祖先後代關係。

資料範圍:\(n\le 2.5\times 10^5\)

思路分析

考慮 dfs 序,維護 \(dfn_u\)\(siz_u\),但每個資訊都要佔 \(19\) 位,我們至多隻能維護其中一個。

顯然 \(dfn\) 更重要一些,因此考慮把 \(siz\) 資訊壓縮到 \(9\) 位之內。

考慮用一個序列去擬合 \(siz\),即找到一個序列 \(f_0\sim f_{511}\),然後找到第一個 \(f_i\ge siz_u\)\(i\),並且給 \(u\) 加入 \(f_i-siz_u\) 個虛擬兒子,這樣就只需要傳遞 \(i\)

這個序列需要滿足 \(f_{511}\ge 2^{19}\) 且個 \(\sum f_i-siz_u\le 2^{19}-n\)

考慮構造等比數列,令 $f_i=\lfloor (1+q)^i\rfloor $,其中 \(q\) 是某個很小的實數。那麼每個點增加的虛兒子不超過 \(q\times siz_u\) 級別,又因為每個點深度不超過 \(19\),因此 \(\sum siz_u\le 19n\),因此樹的總點數不超過 \((1+19q)n\)

事實上不需要嚴格令 \(f_i=\lfloor (1+q)^i\rfloor\),可以令 \(f_i=\max(1,f_{i-1}\times q))+f_{i-1}\),這樣避免了前期增長速度過慢的問題。

可以手動二分一下找到一個合法的 \(q\),實際上取 \(q\in [0,22,0.45]\) 中的實數都符合條件。

時間複雜度 \(\mathcal O(n\log n)\)

程式碼呈現

encoder.cpp

#include<bits/stdc++.h>
#define double long double
#include "Encoder.h"
using namespace std;
const int MAXN=2.5e5+1,C=512;
const double Q=1.023;
vector <int> G[MAXN];
int dfn[MAXN],siz[MAXN],dcnt=0,pw_e[C];
inline void dfs(int p,int fa) {
    dfn[p]=++dcnt,siz[p]=1;
    for(int v:G[p]) if(v!=fa) dfs(v,p),siz[p]+=siz[v];
    int k=lower_bound(pw_e,pw_e+C,siz[p])-pw_e;
    siz[p]=pw_e[k],dcnt=dfn[p]+siz[p]-1;
    Code(p,k+1ll*C*dfn[p]);
}
void Encode(int N,int A[],int B[]) {
    for(int i=0;i<N-1;++i) {
        G[A[i]].push_back(B[i]);
        G[B[i]].push_back(A[i]);
    }
    pw_e[0]=1;
    for(int i=1;i<C;++i) pw_e[i]=max(pw_e[i-1]+1,(int)(pw_e[i-1]*Q));
    dfs(0,0);
}

device.cpp

#include<bits/stdc++.h>
#define double long double
#define ll long long
#include "Device.h"
using namespace std;
const int MAXN=2.5e5+1,C=512;
const double Q=1.023;
int pw_d[C];
void InitDevice() {
    pw_d[0]=1;
    for(int i=1;i<C;++i) pw_d[i]=max(pw_d[i-1]+1,(int)(pw_d[i-1]*Q));
}
int Answer(ll S,ll T) {
    int d1=S/C,s1=pw_d[S%C];
    int d2=T/C,s2=pw_d[T%C];
    if(d2<=d1&&d1<d2+s2) return 0;
    if(d1<=d2&&d2<d1+s1) return 1;
    return 2;
}

L. Dragon 2

Problem Link

題目大意

二維平面上有 \(n\) 個點,和一條線段 \(S\),每個點有 \(1\sim m\) 中的一種顏色。

\(q\) 次詢問 \(x,y\),表示所有顏色為 \(x\) 的點向顏色為 \(y\) 的點連一條射線,有多少條射線與 \(S\) 相交。

資料範圍:\(n\le 30000,q\le 10^5\)

思路分析

首先考慮如何刻畫連線相交,先旋轉座標系使得 \(S\) 連線 \((0,0),(1,0)\)

然後維護每個點 \(i\)\(S\) 構成的三角形的兩個底角 \(\theta_i,\varphi_i\)

  • 如果 \(j\)\(i\) 在 X 軸的同側,那麼 \(i\to j\) 的射線與 \(S\) 相交當且僅當 \(\theta_j\le\theta_i\)\(\varphi_j\le \varphi_i\)
  • 如果 \(j\)\(i\) 在 X 軸的異側,那麼 \(i\to j\) 的射線與 \(S\) 相交當且僅當 \(\theta_j\le \pi-\theta_i\)\(\varphi_j\le \pi-\varphi_i\)

容易發現同一個 \(i\) 對答案的貢獻是一個二維數點問題,維護 X 軸兩側兩側 \((\theta_i,\varphi_i),(\pi-\theta_i,\pi-\varphi_j)\) 分別對應的主席樹即可。

然後考慮怎麼計算答案,顯然的最佳化是優先列舉大小較小的一種顏色,根據經典結論,答案是根號級別的:

  • 如果兩個集合大小都 \(\ge B\),那麼這樣的集合只有 \(\mathcal O\left(\dfrac nB\right)\) 個,因此每個集合最多被算 \(\mathcal O\left(\dfrac nB\right)\) 次,總計算次數為 \(O\left(\dfrac {n^2}B\right)\)
  • 如果有一個集合大小 \(<B\),那麼這一部分計算次數為 \(\mathcal O(qB)\)

均值不等式得到平均後的計算次數為 \(\mathcal O(n\sqrt q)\)

計算幾何處理時注意精度。

時間複雜度 \(\mathcal O(n\sqrt q\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define int long long
#define double long double
using namespace std;
const int MAXN=30001;
const double pi=acosl(-1),eps=1e-20;
vector <array<double,2>> dat[MAXN]; //(x,y) = (p[0],p[1])
vector <array<double,2>> orgP[MAXN][2]; //0: I & II, 1: III & IV
vector <array<double,4>> angs[MAXN][2]; //(ang_x,ang_y,180-ang_x,180-ang_y)
vector <array<int,4>> rnk[MAXN][2]; //rank of {ang[i][j]}

class SegmentTree {
    private:
        struct Node {
            int ls,rs,sum;
        };
        vector <Node> tree;
        int siz;
        inline int Append(int u,int l,int r,int pre) {
            int now=++siz;
            if(l==r) { tree[now].sum=tree[pre].sum+1; return now; }
            int mid=(l+r)>>1;
            if(u<=mid) {
                tree[now].ls=Append(u,l,mid,tree[pre].ls);
                tree[now].rs=tree[pre].rs;
            } else {
                tree[now].ls=tree[pre].ls;
                tree[now].rs=Append(u,mid+1,r,tree[pre].rs);
            }
            tree[now].sum=tree[tree[now].ls].sum+tree[tree[now].rs].sum;
            return now;
        }
        inline int Count(int ul,int ur,int l,int r,int pos) {
            if(!pos||(ul<=l&&r<=ur)) return tree[pos].sum;
            int mid=(l+r)>>1,ans=0;
            if(ul<=mid) ans+=Count(ul,ur,l,mid,tree[pos].ls);
            if(mid<ur) ans+=Count(ul,ur,mid+1,r,tree[pos].rs);
            return ans;
        }
    public:
        vector <int> vals,root;
        int n;
        inline void Build(vector <array<int,2>> &V) {
            sort(V.begin(),V.end());
            tree.resize(20*((int)V.size())+5);
            vals.push_back(0);
            root.push_back(0);
            for(int i=0;i<(int)V.size();++i) {
                int nxt=Append(V[i][1],1,n,root[i]);
                root.push_back(nxt);
                vals.push_back(V[i][0]);
            }
        }
        inline int Query(int x,int y,int opr) {
            if(opr==0) {
                int r=upper_bound(vals.begin(),vals.end(),x)-vals.begin()-1;
                return Count(1,y,1,n,root[r]);
            }
            if(opr==1) {
                int r=lower_bound(vals.begin(),vals.end(),x)-vals.begin()-1;
                return Count(y,n,1,n,root.back())-Count(y,n,1,n,root[r]);
            }
            return 0;
        }
}    TR[MAXN][2][2];
signed main() {
    int n,m;
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;++i) {
        double a,b; int c;
        scanf("%Lf%Lf%lld",&a,&b,&c);
        dat[c].push_back({a,b});
    }
    double d1,e1,d2,e2;
    scanf("%Lf%Lf%Lf%Lf",&d1,&e1,&d2,&e2);
    if(d2<d1) swap(d1,d2),swap(e1,e2);
    double Oang=-atan2l((double)(e2-e1),(double)(d2-d1));
    double sin_ang=sinl(Oang),cos_ang=cosl(Oang);
    const double DX=d1,DY=e1;
    auto Transfer=[&](double &x,double &y) -> void {
        double tx=x-DX,ty=y-DY;
        x=tx*cos_ang-ty*sin_ang,y=tx*sin_ang+ty*cos_ang;
    };
    Transfer(d1,e1),Transfer(d2,e2);
    for(int i=1;i<=m;++i) {
        for(auto p:dat[i]) {
            Transfer(p[0],p[1]);
            int b=(p[1]>0)?0:1;
            orgP[i][b].push_back({p[0],p[1]});
        }
    }
    vector <double> X,Y;
    for(int i=1;i<=m;++i) for(int j:{0,1}) {
        for(auto p:orgP[i][j]) {
            double ang1=atan2l((double)abs(p[1]),(double)p[0]);
            double ang2=atan2l((double)abs(p[1]),(double)(d2-p[0]));
            angs[i][j].push_back({ang1,ang2,pi-ang1,pi-ang2});
            X.push_back(ang1),X.push_back(pi-ang1);
            Y.push_back(ang2),Y.push_back(pi-ang2);
        }
    }
    auto RealEqual=[&](double u,double v) { return fabs(u-v)<=eps; };
    sort(X.begin(),X.end());
    X.erase(unique(X.begin(),X.end(),RealEqual),X.end());
    sort(Y.begin(),Y.end());
    Y.erase(unique(Y.begin(),Y.end(),RealEqual),Y.end());
    for(int i=1;i<=m;++i) for(int j:{0,1}) {
        for(auto p:angs[i][j]) {
            auto Idx=[&](const vector <double> &vals,double RealValue) {
                return lower_bound(vals.begin(),vals.end(),RealValue)-vals.begin()+1;
            };
            rnk[i][j].push_back({Idx(X,p[0]),Idx(Y,p[1]),Idx(X,p[2]),Idx(Y,p[3])});
        }
    }
    for(int i=1;i<=m;++i) for(int j:{0,1}) {
        vector <array<int,2>> vals[2];
        for(auto u:rnk[i][j]) {
            vals[0].push_back({u[0],u[1]});
            vals[1].push_back({u[2],u[3]});
        }
        for(int k:{0,1}) {
            TR[i][j][k].n=Y.size();
            TR[i][j][k].Build(vals[k]);
        }
    }
    int q;
    scanf("%lld",&q);
    while(q--) {
        int u,v,ans=0;
        scanf("%lld%lld",&u,&v);
        if(dat[u].size()<dat[v].size()) {
            for(int s:{0,1}) for(auto w:rnk[u][s]) {
                ans+=TR[v][s][0].Query(w[0],w[1],0);
                ans+=TR[v][s^1][0].Query(w[2],w[3],0);
            }
        } else {
            for(int s:{0,1}) for(auto w:rnk[v][s]) {
                ans+=TR[u][s][0].Query(w[0],w[1],1);
                ans+=TR[u][s^1][1].Query(w[0],w[1],1);
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}