1. 程式人生 > 實用技巧 >青藤程式設計營國慶集訓/S模擬賽solution

青藤程式設計營國慶集訓/S模擬賽solution

Corn#2 題解

T1 奆炮的重生

\(by \ \text{fr200110217102}\)

一句話題意

有一個環,每次可以

  • 合併相鄰兩數\(A\)\(B\),得到的新數\(A+B\)

  • 合併相鄰三數\(A\)\(B\)\(C\),得到的新數\(A \times C - B\)

求最終剩下的數的最大值。

  • \(T \leq 50\)

  • \(n \leq 35\)

  • \(Ans \leq 2^{63}-1\)

演算法一(前三個點):手玩

\(n=1\)

……

\(n=2\)

A+B問題!

\(n=3\)

\(Ans = \max(A+B+C,A \times B-C,A \times C-B,B \times C-A)\)

\(n=4\) / \(n=5\)

把所有情況都枚舉出來就好了!反正一共才幾十種。

演算法二(前五個點):暴力

DFS。

列舉合併的位置進行操作。

環長為\(n\)的時候有\(\text{O} (n)\)種合併的可能,所以複雜度是\(\text{O}(n!)\)級別。

演算法三(所有點):標算

區間DP。

這麼明顯的“相鄰”“合併”還想不到區間DP嗎

先破壞成鏈,把環複製一份,然後直接在\(2n\)的序列上做區間DP。

我們看到了減法,還看到了有正有負……所以要維護最大和最小值了。

\(f[l][r]=\)\([l,r]\)合併為一個數的最大值,\(g[l][r]=\)\([l,r]\)

合併為一個數的最小值

\(f\)的轉移有3種:

\(f[l][k]+f[k+1][r]\)(合併兩個)

\(f[l][i] \times f[j][r]-g[i+1][j-1]\)

\(g[l][i] \times g[j][r]-g[i+1][j-1]\)(合併三個,最大\(\times\)最大/最小\(\times\)最小。注意減最小值才是最大值)

\(g\)的轉移有5種:最大\(\times\)最大,最小\(\times\)最小,最大\(\times\)最小,最小\(\times\)最大都有可能成為最小值。減的時候要減最大值。

本題資料比較水,\(g\)只用最大\(\times\)最小和最小\(\times\)

最大轉移也可以做。

最終答案就是\(\max(f[1][n],f[2][n+1],...,f[n][2*n-1])\)

時間複雜度:\(\text{O}\left(n^4\right)\)

std部分程式碼

void solve(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%lld",&a[i]),a[i+n]=a[i];
    m=n<<1;
    for(int i=1;i<=m;++i)s[i]=s[i-1]+a[i];
    for(int i=1;i<=m;++i)for(int j=i;j<=m;++j)
        f[i][j]=g[i][j]=s[j]-s[i-1];
    for(int len=2;len<=n;++len)for(int l=1;l<=m-len+1;++l){
        int r=l+len-1;
        for(int i=l;i<r;++i){
            cmax(f[l][r],f[l][i]+f[i+1][r]);
            cmin(g[l][r],g[l][i]+g[i+1][r]);
        }
        for(int i=l;i<r-1;++i)for(int j=i+2;j<=r;++j){
            cmax(f[l][r],f[l][i]*f[j][r]-g[i+1][j-1]);
            cmax(f[l][r],g[l][i]*g[j][r]-g[i+1][j-1]);
            cmin(g[l][r],f[l][i]*f[j][r]-f[i+1][j-1]);
            cmin(g[l][r],g[l][i]*g[j][r]-f[i+1][j-1]);
            cmin(g[l][r],f[l][i]*g[j][r]-f[i+1][j-1]);
            cmin(g[l][r],g[l][i]*f[j][r]-f[i+1][j-1]);
        }
    }
    ll ans=-1e18;
    for(int i=1;i<=n;++i)cmax(ans,f[i][i+n-1]);
    printf("%lld\n",ans);
}

一個假的標算

如果你寫了區間DP,但是你每次都重新做了一遍區間DP,

那麼恭喜你可以獲得80~90分的高分!

總結

定位:送分題

這100分是出題人對選手慷慨的饋贈。

出題人希望這道題能為你未來的NOIP之路提供一個有力的援助。


T2 幽香的宴會

\(by \ \text{Wy12121212}\)

一句話題意

給出一個圖,每個點有權值,每條邊有海拔 \(p_i\)

給出一常數 \(k\)\(T\) 次詢問在只保留海拔大於等於 \(P_i\) 的邊時 \(c_i\) 所在連通塊中前 \(k\) 大點權之和為多少。

  • \(n,m,T\leq 2 \times 10^5\)

  • \(k \leq 10^4\)

  • 詢問不強制線上

Case 1 ~ 16

  • \(n,m,T \leq 10^3\)

  • \(k\leq 10^2\)

考慮暴力。

我們直接從 \(c_i\) 遍歷(只走可行邊),將訪問到的點權塞入小根堆中,當小根堆大小大於 \(k\) 的時候彈出堆頂即可

或者直接拿個陣列存一下,遍歷完後排個序取出前 \(k\) 大累加即可

時間複雜度 \(\text{O}\left( Tn \log k \right) \text{ or } \text{O}\left( Tn \log n \right)\)

Case 17

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k=1\)

  • \(P_i \geq P_{i-1}\)

    \(P_i\) 單調不減意味著邊會被一條一條去掉,這個我們不方便維護。

因此將詢問倒過來做,那麼 \(P_i \leq P_{i-1}\) ,所以我們是在一條一條向圖中加邊。

我們使用並查集經行維護,因為 \(k = 1\) 所以我們只需要維護每個集合的最大值,在並查集合並 \(fa\) 的時候順便合併最大值即可。

時間複雜度 \(\text{O}\left(n \alpha \left(n\right)\right)\)

Case 18 ~ 19

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k=1\)

\(P_i\) 沒有單調性,那麼我們將詢問按照 \(P_i\) 從大到小排序,將每條邊按照 \(p_i\) 從大到小排序。

這樣我們維護兩個指標 \(i,j\), 分別指向正在處理的詢問和第一個還沒有加入圖中的邊,每次 \(i\) 右移一位,然後 \(j\) 右移直至 \(p_j < P_i\) 即可, \(j\) 在右移的過程中要將訪問到的邊全部加入圖。

同樣使用並查集維護連通性即最大值。

時間複雜度 \(\text{O}\left(n \log n\right)\)

Case 20

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k=2\)

  • \(P_i \geq P_{i-1}\)

類似於 Case 17 的做法,但是 \(k = 2\) 意味著我們要維護每個集合的最大值和次大值。

那麼在合併兩個集合的時候討論一下即可得到新的最大值和次大值了。

或者更通用的方法是將兩個集合的最大次大值塞到大根堆(優先佇列)中再取出最大的兩個即可。

複雜度同 Case 17

Case 21 ~ 22

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k=2\)

將前兩種做法結合起來就可以了。

Case 23

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k \leq 10^4\)

  • \(P_i = 0\)

比較靠後的送分點

由於 \(P_i = 0\) ,所以沒有邊不能走,我們直接先建出整個圖,加邊用並查集維護。

之後將每個點的權值加入到它所在連通塊所對應的小根堆(優先佇列)中並將該連通塊的當前答案加上點權,同樣的在小根堆大小超過 \(k\) 的時候將連通塊的答案減去堆頂權值並彈出堆頂即可。

查詢的時候直接輸出 \(c_i\) 所在所在連通塊的答案即可。

時間複雜度 \(\text{O}\left(n \log k\right)\)

Case 24 ~ 25:

  • \(n,m,T \leq 2 \times 10^5\)

  • \(k \leq 10^4\)

(我們可以通過離線排序保證 \(P_i\) 的單調性,所以兩個 Case 本質相同,故不分開討論)

這是防AK的部分分,不感興趣的同學可以跳過

首先,我們大概要延續 Case 21 ~ 22 的做法,所以我們需要對每個集合維護前 \(k\) 大的值,接下來考慮一下如何暴力合併兩個集合前 \(k\) 大的值。

  • 對每個集合維護一個小根堆(優先佇列)

  • 兩個集合合併時,不斷彈出較小的小根堆堆頂直至兩個小根堆的大小之和小於等於 \(k\) ,那麼現在兩個堆內的元素就是合併後前 \(k\) 大的值

  • 將一個堆中的所有元素暴力彈出並加到另一個堆中

正解就是在這種做法的基礎上使用了並查集的按秩合併,即將小的集合的堆暴力合併到大的集合的堆中。

假設 \(n,k\) 同級的最劣情況,那麼沒有點會被彈出,根據按秩合併的原理,每個點最多被合併 \(\log n\) 次,而單次合併複雜度為 \(\log k\) ,所以時間複雜度為 \(\text{O}\left(n \log n \log k\right)\)

std部分程式碼

int main(){
    read(n),read(m);
    for(int i=1;i<=n;++i){
        f[i]=i,s[i]=1;
        read(x),sum[i]=x;
        q[i].push(x);
    }
    for(int i=1;i<=m;++i)e[i].init();
    read(T),read(k);
    for(int i=1;i<=T;++i)g[i].init(i);
    sort(e+1,e+m+1);
    sort(g+1,g+T+1);
    for(int i=1,j=1;i<=T;++i){
        while(j<=m&&e[j].p>=g[i].p)
            merge(e[j].x,e[j].y),++j;
        ans[g[i].i]=sum[fnd(g[i].x)];
    }
    for(int i=1;i<=T;++i)wtln(ans[i]);
}
inline int fnd(int x){
    return x==f[x]?x:f[x]=fnd(f[x]);    
}
inline void merge(int x,int y){
    x=fnd(x),y=fnd(y);
    if(x==y)return;
    if(s[x]>s[y])x^=y^=x^=y;
    f[x]=y,s[y]+=s[x],sum[y]+=sum[x];
    while(!q[x].empty())q[y].push(q[x].top()),q[x].pop();
    while(s[y]>k)sum[y]-=q[y].top(),q[y].pop(),--s[y];    
}

T3 玉米的豐收

\(by \ \text{2017sjb.}\)

一句話題意

在圖中任選一點(點上,邊上均可),使該點到圖中其他點的距離的最大值最小。

求該最小值。

  • \(n \leq 300\)
  • \(m \leq 20000\)
  • \(w_i \leq 100000\)

Case 1 ~ 8

  • \(n \leq 5\)
  • \(m \leq 6\)

大力討論即可。

Case 1 2 4 7 9 11 13 15 18

  • \(m = n - 1\)

容易發現在樹的直徑的中點處最優。

直接求出樹的直徑,再除2即可。

時間複雜度: \(\text{O} \left( n \right)\)

Case 9 ~ 12

  • \(n \leq 50\)
  • \(m \leq 400\)
  • \(w_i \leq 1000\)

受樹的情形的啟發,繼續分析,發現最優位置具有幾個性質:

  • 最優位置到至少兩個點的距離等於最遠距離【若只有一個點,則將最優位置向靠近這個點的方向移動會使最遠距離變小】。
  • 到最優位置距離等於最遠距離的點中至少存在一對點在最優位置的兩側(最優位置在以這對點為兩端的一條鏈的中點處)【若所有點都在同側,則將最優位置向這一側移動將使最遠距離變小】。

因此最遠距離的最小值等於圖中某條鏈的長度\(/2\),一定是 \(0.5\) 的正整數倍,那麼把所有邊權\(\times 2\),就能使最遠距離變為整數。

考慮列舉最優位置所屬的邊,到邊端點的距離,用 SPFA/Dijkstra 求出最遠距離即可。

時間複雜度:\(\text{O} \left( km^2w \right)\)

繼續觀察發現,可以用 Floyd 預處理求出圖中任意兩點間的最短距離,那麼點 \(i\) 到最優位置的距離可以快速求出:

設 點 \(i\) 到點 \(j\) 最短距離為 \(d[i][j]\)

設 最優位置所在邊為 \(x \leftrightarrow y\) ,邊權為 \(len\)

設 最優位置到點 \(x\) 距離為 \(dis\)\(dis \in \left[ 0,len \right]\)

則 最優位置到點 \(y\) 距離為 \(len - dis\)

則 點 \(i\) 到最優位置的距離為 \(\min \left( d[i][x] + dis , d[i][y] + len - dis \right)\)

時間複雜度:\(\text{O} \left( n^3 + nmw \right)\)

可以通過 Case 13

繼續觀察發現,可以快速排除一些最優位置不可能出現的邊:

考慮一條邊 \(x \leftrightarrow y\) ,邊權為 \(len\)

\(d[x][y]<len\) 即:圖中存在一條更短的路徑,那麼最優位置不可能出現在這條邊上【若最優路徑在這條邊上,則把最優位置換到從 \(x\)\(y\) 的更短路徑上到 \(x\) 距離不變的位置,再向靠近 \(x\) 的方向移動一段距離,最遠距離單調不增】

由於資料隨機,使用這個剪枝可以再通過 Case 14

若每次再使用當前最優答案進行剪枝,那麼可以暴力踩標算 AC 本題。

這是出題人推薦的解法。

部分程式碼

struct edge{
    int x,y,z;
    inline void init(){
        scanf("%d %d %d\n",&x,&y,&z),z<<=1;
        d[x][y]=d[y][x]=z;
    }
    inline bool operator<(const edge& e)const{
        return z<e.z;
    }
    inline bool poss(){
        return d[x][y]==z;    
    }
}e[M];
inline int check(int x,int y,int dx,int dy) {
    int res=0;
    for(int i=1;i<=n;++i)
        res=max(res,min(d[x][i]+dx,d[y][i]+dy));
    return res;
}
inline void judge(int x,int y,int dis) {
    if(dis>=2*ans)return;
    for(int i=1;i<=n;++i)if(min(d[i][x],d[i][y])>=ans)return;
    int i=0;
    for(;i<=dis&&i<ans;++i)
        ans=min(ans,check(x,y,i,dis-i));
    i=max(i,dis-ans+1);
    for(;i<=dis;++i)
        ans=min(ans,check(x,y,i,dis-i));
}
int main() {
    //   ...
    for(int i=1; i<=m; ++i)e[i].init();
    //   ...
    std::sort(e+1,e+m+1);
    for(int i=1; i<=m; ++i)
        if(e[i].poss())judge(e[i].x,e[i].y,e[i].z);
    //   ...
}

以下為防AK部分,請謹慎閱讀。

Case 13 ~ 17

  • \(n \leq 200\)
  • \(m \leq 5000\)
  • \(w_i \leq 10000\)

考慮對上一解法做出優化:

  • 列舉最優位置所屬的邊,用二分求出最優位置在當前邊上時的最遠距離的最小值。

二分方法:

\(len\)\(dis\) 均沿用上一解法中的定義

設 當前二分的最遠距離為 \(mid\)

則 存在最優位置等價於存在 \(dis\) 使任意點 \(i\) 均滿足:\(mid \leq \min \left( d[i][x] + dis , d[i][y] + len - dis \right)\)

整理得 \(dis \geq mid - d[i][x]\)\(dis \leq len + d[i][y] -mid\)

\(dis \in \left[ 0,mid - d[i][x] \right] \cup \left[len + d[i][y] -mid,len\right]\)

\(n\) 個集合的交集不為空集則存在最優位置,反之不存在

但對 \(n\) 個這樣的集合求交集很困難,考慮這些集合的特殊性質:
\(\left[0,len\right]\) 為全集,第 \(i\) 個集合的補集為 \(\left(mid - d[i][x],len + d[i][y] -mid\right)\)

原先的 \(n\) 個集合的交集不為空集等價於 \(n\) 個補集的並集不為全集,問題轉化為了求 \(n\) 個開區間的並集,可以使用快速排序在 \(\text{O} \left( n \log n \right)\) 時間內解決

時間複雜度:\(\text{O} \left( mn \log n \log w \right)\)

Case 18 ~ 20

  • \(n \leq 300\)
  • \(m \leq 20000\)
  • \(w_i \leq 100000\)

上一解法還可以進一步優化:

設 當前在邊 \(x \leftrightarrow y\) 上二分

要進行求並的 \(n\) 個區間都形如:\(\left(mid - d[i][x],len + d[i][y] -mid\right)\)

注意到兩個區間左端點的相對大小關係,與 \(mid\) 的取值無關,因此可以在二分之前把所有區間預先排序好,這樣一次二分的複雜度就可以優化成 \(\text{O} \left( n \right)\)

時間複雜度:\(\text{O} \left( mn \left( \log n +\log w \right) \right)\)

std部分程式碼

std::pair<int,int> b[N];
inline bool check(int dis,int lim) {
    for(int i=1,R=0; i<=n; ++i) {
        if(b[i].first+lim>=R)return 1;
        R=max(R,b[i].second-lim);
        if(R>dis)return 0;
    }
    return 1;
}
inline void judge(int x,int y,int dis) {
    for(int i=1;i<=n;++i){
        if(min(d[i][x],d[i][y])>=ans)return;
        b[i]=std::make_pair(-d[i][x],dis+d[i][y]);
    }
    std::sort(b+1,b+n+1);
    if(!check(dis,ans))return;
    int L=0,R=ans-1,mid;
    while(L<=R) {
        mid=(L+R)>>1;
        if(check(dis,mid))ans=mid,R=mid-1;
        else L=mid+1;
    }
}

Case 6 10 14 19

三分需要保證原函式為單峰/谷函式。

本題中的函式並非單谷函式,但在隨機資料下大多呈單谷趨勢。

這些測試點經過了一些構造,可能使一些三分的解法 WA