CSP-S 2019 Solution

Day1-T1 格雷碼(code)

格雷碼是一種特殊的 \(n\) 位二進制串排列法,要求相鄰的兩個二進制串剛好有一位不一樣,環狀相鄰。node

生成方法:數組

  1. \(1\) 位格雷碼由兩個 \(1\) 位的二進制串組成,順序爲 \(0,1\)
  2. \(n+1\) 位的格雷碼的前 \(2^n\) 個串,是由 \(n\) 位格雷碼順序排列再加前綴 0 組成。
  3. \(2^n\) 個串,由 \(n\) 位格雷碼逆序排列加前綴 1 組成。

\(n\) 位格雷碼的第 \(k\) 個串。函數

\(1\leq n\leq 64,0\leq k\leq 2^n\) .優化

Thoughts & Solution

考慮一個跟康託展開很是類似的思路。spa

首先看第一位,若是是 1 那麼說明它前面已經能夠肯定至少排了 \(2^n\) 個 0 開頭的二進制串。code

那麼這樣就能夠肯定第一位是 0 仍是 1 ,看 \(k\) 的大小就行了。遞歸

後面的也是同理,每次判斷完以後:隊列

  • 若是是 1 就把 \(k\) 減去 \(2^i\) ,而後因爲這一位是 1 ,因此後面的都須要逆序,直接用總個數減去減完以後的 \(k\) (注意,這個逆序是相對於下一層而言的,因此應該是 \(2^i-(k-2^i)-1\) ,也就是 \(2^{i+1}-k-1\)
  • 不是 1 ,就不動,給出 0 ,而後繼續下一位便可。

複雜度是 \(\mathcal{O}(n)\) 的。(不過這種題也不須要考慮這個吧)字符串

最後:通過 CSP-S2020 ,我發現 \(k<2^n\leq 2^{64}\)get

寫代碼的時候注意溢出問題,要開 unsigned long long 特別是左移的地方要注意。

//Author: RingweEH
#define ull unsigned long long
const int N=70;
int n,a[N];
ull k;

int main()
{
    n=read(); scanf( "%llu",&k );

    ull now=1ull<<(n-1);
    for ( int i=n-1; i>=0; i-- )
    {
        if ( (k>>i)&1 ) a[i]=1,k=(now<<1)-k-1;
        else a[i]=0;
    }

    for ( int i=n-1; i>=0; i-- )
        printf( "%d",a[i] );
    
    return 0;
}

Day1-T2 括號樹(brackets)

給定一棵以 \(1\) 爲根的括號樹,每一個點恰有一個 () ,定義 \(s(i)\) 爲將根節點到 \(i\) 號點的簡單路徑按通過順序排列造成的字符串。

\(k(i)\) 表示 \(s(i)\) 中互不相同的子串是合法括號串的個數。求 \(\forall1\leq i\leq n,\sum i\times k(i)\) ,這裏的求和表示異或和。

\(n\leq 5e5\)

Thoughts & Solution

終於補完模擬賽來繼續寫題了

題外話:今天模擬賽也有一道括號匹配題,可是是奇妙的貪心,要寫兩個棧+一個雙端隊列(

STL永遠的神!

顯然若是對這棵樹進行 DFS ,那麼根到 \(i\) 的路徑上的點能夠用棧獲得。

那麼一遍 DFS 就能夠處理出根到 \(i\) 的路徑上 互不相同的子串是合法括號串 的個數。

\(endpos[i]\) 爲以節點 \(i\) 結尾的,根到 \(i\) 中互不相同的合法括號子串的個數。相似括號匹配的思路,若是當前爲左括號那麼直接進棧;若是是右括號且棧不爲空,那麼棧頂就能和當前點配對,這樣就造成了一個新的合法子串 sta.top(),i ,那麼當前節點的 \(endpos\) 就能夠由這一對括號以前的東西推知。

\(fa[i]\) 表示括號樹上點 \(i\) 的父親節點,那麼 \(endpos[i]=endpos[fa[sta.top()]]+1\) (由於 \(endpos[fa[sta.top()]]\) 這些串都能和當前這一對括號接起來,成爲一個新的合法子串;或者當前這個單獨成串)

最後 \(k(i)\) 就是根到 \(i\) 的路徑上全部的 \(endpos\) 之和,這個也能夠在 DFS 的時候順帶求出來。

注意遞歸完以後要記得還原,pop 掉的左括號搞回去,push 的左括號拿出來。

//Author: RingweEH
const int N=5e5+10;
int n,fa[N],endpos[N];
ll f[N];
stack<int> sta;
vector<int> son[N];
char s[N];

void dfs( int u )
{
    bool pu=0; int las=0;
    if ( s[u]=='(' ) sta.push( u ),pu=1;
    else if ( !sta.empty() ) { endpos[u]=endpos[fa[sta.top()]]+1; las=sta.top(); sta.pop(); }
    f[u]=f[fa[u]]+endpos[u];
    for ( int i=0; i<son[u].size(); i++ )
        dfs( son[u][i] );
    if ( pu && sta.top()==u ) sta.pop();
    if ( las ) sta.push( las );
}

int main()
{
    n=read(); scanf( "%s",s+1 );
    for ( int i=2; i<=n; i++ )
        fa[i]=read(),son[fa[i]].push_back(i);
    
    endpos[1]=0; f[1]=0; dfs( 1 );

    ll ans=0;
    for ( int i=1; i<=n; i++ )
        ans^=(i*f[i]);

    printf( "%lld\n",ans );

    return 0;
}

Day1-T3 樹上的數(tree)

給定一棵大小爲 \(n\) 的數,初始時每一個節點上都有一個 \(1\sim n\) 的數字,且每一個 \(1\sim n\) 的數字都只 剛好 在一個節點上出現。

進行 剛好 \(n-1\) 次刪邊操做,每次操做須要選一條 未被刪去的邊 ,交換兩個端點的數字,並刪邊。

刪完後,按數字 \(1\sim n\) 的順序將所在節點編號依次排列獲得排列 \(P_i\) ,求能獲得的字典序最小的 \(P_i\) .

\(n\leq 2000\)

Thoughts & Solution

這種 「字典序最小」的題一看就很像貪心。

題外話:今天模擬賽也有 「字典序最小」的貪心題,但是我一連胡錯了兩次,最後思路對了還調了半天(

要想貪心,確定是讓某個小編號儘量地在最後的排列中獲得小的權值。那麼考慮如何讓一個數字最終達到某個特定的位置。

假設如今有一條路徑 :\(start\to a\to b\to c\to d\to end\) ,並將這些邊從左到右依次標號爲 \(1,2,3,4,5\)

那麼,假設咱們如今想要把 \(start\) 節點上的數字轉移到 \(end\) 節點上。能夠發現:

  • 對於全部和 \(start\) 相連的邊,\(1\) 必定是被刪除的第一條邊(不然 \(start\) 原先的權值就被轉移沒了)
  • 對於全部和 \(end\) 相連的邊,\(5\) 必定是被刪除的第一條邊(不然 \(start\) 搞過來的權值就會被轉移沒了)
  • 對於中間點 \(a,b,c,d\) ,在它們的刪邊序列中,\(1\)\(2\)\(2\)\(3\)\(3\)\(4\)\(4\)\(5\) 必定相鄰(防止權值中途被轉移走)

因爲要前面的數儘量小,那麼枚舉填數的時候必定是從小到大的。每次利用以上的性質判斷是否可以填到這個位置,直到找到一個能填且最小的位置,並加上這個點給後面的限制。

看到圖論裏面的限制其實一個很天然的想法就是:連邊爲限制 ,可是這裏的限制是針對邊的,那麼就能夠考慮把邊轉化成點。

對原樹上的每個點建一張圖,圖上每一個點表明連的一條邊,並記錄這個點欽定的第一條邊和最後一條邊。這張圖上的一條有向邊表示出點在入點以後立刻選擇。點與點之間建的圖是獨立的。

考慮什麼狀況下會出現矛盾。

  • 圖不能被分割成獨立的若干條鏈(這樣就會有一條邊後面要連多條邊,或是出現環,顯然不合法)

  • 欽定的第一個點和最後一個點有入/出邊(顯然不合法)

  • 第一個點和最後一個點在同一條鏈上,可是還有點不在這條鏈中。(已經造成了完備惟一的刪邊方案,可是有邊還沒刪)

這些條件的矛盾分別表現爲:

  • 首先是加邊的時候兩點不能已經連通,這個並查集判一下就行了;而後出點不能有出邊,入點不能有入邊,bool 數組記錄便可。
  • 直接判斷
  • 這條邊會讓欽定的起點和終點合併,可是目前不連通的個數還大於 2

而後各類判斷就行了。題目很 毒瘤 細節(也有多是我寫煩了),實現時要注意。

代碼中有詳細的註釋,若是發現本身掛了能夠看看註釋,找找有沒有漏掉的條件。

我發現我最近善於把題目寫得碼農(

//Author: RingweEH
const int N=2010;
int n,pos[N],head[N],tot;   //pos:數字 i 初始節點位置
struct edge
{
    int to,nxt;
}e[N<<1];
struct node_graph       //每一個點所創建的圖
{
    int fir,las,num,fa[N];      //欽定的第一條邊,最後一條,邊(點)數,並查集
    bool ine[N],oue[N];     //是否有入/出邊
    void clear() { fir=las=num=0; for ( int i=1; i<=n; i++ ) fa[i]=i,ine[i]=oue[i]=0; }
    int find( int x ) { return x==fa[x] ? x : fa[x]=find(fa[x]); }
}g[N];

void add( int u,int v )
{
    e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; g[u].num++; 
    e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot; g[v].num++;
}

int dfs1( int u,int fro_edge )
{
    int res=n+1;
    if ( fro_edge && (!g[u].las || g[u].las==fro_edge ) )   //尚未終點或者終點就是這條邊
    {
        if ( !g[u].oue[fro_edge] && !(g[u].fir && g[u].num>1 && g[u].find(fro_edge)==g[u].find(g[u].fir)) )
        //這條邊(點)在這個點的圖裏面尚未出邊,並且不能:
        //有起點,總點數大於1,且已經在一條鏈裏面了
            res=u;
    }
    for ( int i=head[u]; i; i=e[i].nxt )
    {
        int v=e[i].to,to_edge=i/2;
        if ( fro_edge==to_edge ) continue;      //防止沿着雙向邊搜回去,tot是從1開始的,因此同一條雙向邊/2向下取整是同樣的
        if ( !fro_edge )        //前面沒有鏈的狀況
        {
            if ( !g[u].fir || g[u].fir==to_edge )   //沒有欽定起點,或者起點就是當前邊
            {
                if ( g[u].ine[to_edge] ) continue;  //若是有入邊了就不能當起點
                if ( g[u].las && g[u].num>1 && g[u].find(to_edge)==g[u].find(g[u].las) )
                    continue;   //起點和終點已經在一條鏈裏面了
                res=min( res,dfs1( v,to_edge ) );
            }
            else continue;
        }
        else    //前面有鏈,日後接的狀況
        {
            if ( fro_edge==g[u].las || to_edge==g[u].fir || g[u].find(fro_edge)==g[u].find(to_edge) )
                continue;   //若是上一條鏈的尾點是終點,那麼後面不能接鏈;若是這條邊是起點,那麼不能被接;
                //若是已經在一條鏈上了,也不能被接
            if ( g[u].oue[fro_edge] || g[u].ine[to_edge] ) continue; //已經接過了
            if ( g[u].fir && g[u].las && g[u].num>2 && g[u].find(fro_edge)==g[u].find(g[u].fir) 
                && g[u].find(to_edge)==g[u].find(g[u].las) ) continue;
            //從起點來的鏈,接上去終點的鏈,且還有點不在鏈上
            res=min( res,dfs1( v,to_edge ) );
        }
    }
    return res;
}

int dfs2( int u,int fro_edge,int endpos )
{
    if ( u==endpos ) { g[u].las=fro_edge; return 1; }  //到終點了,使命完成
    for ( int i=head[u]; i; i=e[i].nxt )
    {
        int v=e[i].to,to_edge=i/2;
        if ( fro_edge!=to_edge )
        {
            if ( dfs2( v,to_edge,endpos ) )     //後面可行
            {
                if ( !fro_edge ) g[u].fir=to_edge;      //前面沒有了,這個就是起點
                else
                {   //更新有無出入邊的限制,並查集合並
                    g[u].oue[fro_edge]=g[u].ine[to_edge]=1; g[u].num--;
                    g[u].fa[g[u].find(fro_edge)]=g[u].find(to_edge);
                }
                return 1;
            }
        }
    }
    return 0;
}

int main()
{
    int T=read();
    while ( T-- )
    {
        tot=1; memset( head,0,sizeof(head) );

        n=read();
        for ( int i=1; i<=n; i++ )
            g[i].clear(),pos[i]=read();
        for ( int i=1,u,v; i<n; i++ )
            u=read(),v=read(),add( u,v );
        
        if ( n==1 ) { printf( "1\n" ); continue; }
        int p;
        for ( int i=1; i<=n; i++ )
        {
            p=dfs1( pos[i],0 ); dfs2( pos[i],0,p );     //1用來搜方案,2用來加限制
            printf( "%d ",p );
        }
        printf( "\n" );
    }

    return 0;
}

Day2-T1 Emiya 家今天的飯(meal)

\(1\sim n\) 種烹飪方法和 \(1\sim m\) 種食材,使用 \(i\) 方法,食材爲 \(j\) 的一共有 \(a_{i,j}\) 道菜。

對於一種包含 \(k\) 道菜的方案而言:

  • \(k\ge 1\)
  • 每道菜的烹飪方法不一樣
  • 每種食材最多出如今 \(\Big\lfloor \dfrac{k}{2}\Big\rfloor\) 道菜中

求有多少種不一樣的搭配方案,對 \(998244353\) 取模。\(1\leq n\leq 100,1\leq m\leq 2000\)

Thoughts & Solution

對於方陣 \(a\) ,題目要求就至關因而:

  • 要取 \(k\ge 1\) 個數
  • 每行只能取一個
  • 每列只能取不超過 \(k\div 2\) 個。

考慮容斥,那麼就是:每行至多取一個的方案 - 取了 0/1 個的方案 - 存在一列取了超過半數的方案(顯然這樣的列至多有一個)

對於每行至多取一個的總方案,來一遍 DP ,令 \(g[i][j]\) 表示到第 \(i\) 行,取了 \(j\) 個的方案數,\(sum[i]=\sum a_{i,j}\) 那麼有:

\[g[i][j]=g[i-1][j-1]\times sum[i]+g[i-1][j](能夠開滾動維護) \]

發現 取了 1 個的方案 其實能夠直接在 存在一列取了超過半數的方案 裏面統計掉,由於必定是超過半數的。

沒有取的方案直接不加上就行了。

而後就能夠暴力枚舉超過半數的材料是哪一個,進行DP。

\(f[i][j][k]\) 表示前 \(i\) 行,取了 \(j\) 個,其中超過半數的 \(x\) 取了 \(k\) 個( \(\Big\lfloor \dfrac{j}{2}\Big\rfloor <k\)),枚舉到 \(pos\) 這道菜取了超過半數。

轉移挺好想的,就是三種狀況:

  • 不取
  • 取了除 \(pos\) 外的任意一個
  • 取了 \(pos\)

轉移方程:

\[f[i][j][k]=f[i-1][j][k]+f[i-1][j-1][k]\times (sum[i]-a[i][pos])+f[i-1][j-1][k-1]\times a[i][pos] \]

對於每一個 \(pos\) ,對答案的貢獻就是 \(\sum_{i=0}^n\sum_{j=\lfloor i/2\rfloor+1}^i f[k][i][j]\) .

這樣的複雜度是 \(\mathcal{O}(n^3m)\) 的,能獲得 84 分的好成績( 在考場上已經至關可觀了……

而後考慮優化。發現合法狀態只有 \(2\times k>j\) 的部分,也就是說你徹底不須要知道 \(j,k\) 的具體值,因此能夠把狀態搞成 \(2k-j\) ,省掉一維的枚舉時間和空間。

那麼方程就是:

\[f[i][j(2k-j)]=f[i-1][j]+f[i-1][j+1(2k-j+1)]\times (sum[i]-a[i][pos])+f[i-1][j-1(2k-j-1)]\times a[i][pos] \]

(數組下標內的小括號表示根據原先的 \(j,k\) 定義,這個下標的值)

(注意,這裏的合法狀態指的是最終對答案有貢獻的部分,從轉移方程易知 \(2k\leq j\) 的部分仍是有用的,能夠經過若干次 \(j-1\) 部分的轉移貢獻到合法狀態裏面去)

複雜度是 \(\mathcal{O}(n^2m)\) .

實現的時候注意減法取模……由於這個掛成 88 了qaq

//Author: RingweEH
const int N=110,M=2010;
const ll Mod=998244353;
int n,m;
ll a[N][M],g[N],sum[N],f[N][N<<1];

void add( ll &t1,ll t2 )
{
    t1=(t1+t2);
    if ( t1>Mod ) t1-=Mod;
}

int main()
{
    n=read(); m=read();
    for ( int i=1; i<=n; i++ )
        for ( int j=1; j<=m; j++ )
            a[i][j]=read(),add( sum[i],a[i][j] );
    
    memset( g,0,sizeof(g) ); g[0]=1;
    for ( int i=1; i<=n; i++ )
        for ( int j=i; j>=1; j-- )
            add( g[j],g[j-1]*sum[i]%Mod );
    ll ans=0;
    for ( int i=1; i<=n; i++ )
        add( ans,g[i] );
    for ( int pos=1; pos<=m; pos++ )
    {
        memset( f,0,sizeof(f) ); f[0][n]=1;
        for ( int i=1; i<=n; i++ )
            for ( int j=1; j<=n+i; j++ )
            {
                f[i][j]=f[i-1][j];
                add( f[i][j],f[i-1][j+1]*(sum[i]+Mod-a[i][pos])%Mod );
                add( f[i][j],f[i-1][j-1]*a[i][pos]%Mod );
            }
        for ( int i=n+1; i<=n*2; i++ )
            add( ans,Mod-f[n][i]);
    }

    printf( "%lld\n",ans );
    return 0;
}

Day2-T2 劃分(partition)

給定一個長爲 \(n\) 的序列 \(a_i\) ,對於一組規模爲 \(u\) 的數據,代價爲 \(u^2\) .你須要找到一些分界點 \(1\leq k_1<k_2<...<n\) ,使得:

\[\sum_{i=1}^{k_1}a_i\leq \sum_{i=k_1+1}^{k_2} a_i\leq \dots\leq \sum_{i=k_p+1}^n a_i \]

\(p\) 能夠爲 \(0\) 且此時 \(k_0=0\) .而後要求最小化::

\[(\sum_{i=1}^{k_1}a_i)^2+(\sum_{i=k_1+1}^{k_2}a_i)^2+\dots +(\sum_{i=k_p+1}^n a_i)^2 \]

求這個最小的值。

(數據生成方式見題面)

\(n\leq 4e7,1\leq a_i\leq 1e9,1\leq m\leq 1e5,1\leq l_i\leq r_i\leq 1e9,0\leq x,y,z,b_1,b_2\leq 2^{30}\)

Thoughts & Solution

難想好寫的典型案例(其實也不難……)

一個顯然的想法是DP分組。因爲這道題跟組數沒有關係,因此能夠修改一下常規的式子。

\(f[i][j]\) 爲對前 \(i\) 個進行分組,最後一組爲 \([j+1,i]\) 的最小代價,\(sum[i]\) 爲序列前綴和。

有方程:

\[f[i][j]=\min\{f[k][j]+(\sum_{l=j+1}^ia_l)^2\}=\min\{f[k][j]+(sum[i]-sum[j])^2\} \]

複雜度爲 \(\mathcal{O}(n^3)\) .問題出在上一個斷點要一個一個枚舉 \(k\) 獲得。考慮如何加速這個過程。

注意到 「平方之和」必定比 「和的平方」要小。因此把最後一段拆成幾段(在知足遞增的狀況下)答案必定不會變劣。

也就是說最優解的方案必定是合法的裏面 最後一段最短 的一種。

那麼這時候的 \(k\) 就是肯定的,數組就省掉了一維變成 \(f[i]\) .記錄一個 \(las[i]\) 表示 \(f[i]\) 的方案中上一段的末尾。

方程就是:

\[f[i]=\min\{f[j]+(s[i]-s[j])^2\},\\\\ j 知足 sum[j]-sum[las[j]]\leq sum[i]-sum[j]. \]

複雜度 \(\mathcal{O}(n^2)\)這樣已經實現了36分到64分的巨大飛躍(

然而對於 \(n\leq 4e7\) ,加上常數的話複雜度得是線性的……繼續優化。😔

注意上面的 \(j\) 的條件式。

\[sum[j]-sum[las[j]]\leq sum[i]-sum[j]=>sum[i]\ge 2\times sum[j]-sum[las[j]] \]

是否是清新可人的樣子 你會發現若是一個 \(j\) 對於 \(i\) 知足上式,因爲前綴和遞增,顯然對 \(i+1\) 也知足上式,所以可行決策點的範圍必定是左端點爲 \(1\) 的一個區間,且隨着 \(i\) 的增大,這個區間的右端點遞增(顯然)。

咱們用一個函數 \(g(j)=2\times sum[j]-sum[las[j]]\) 來表示右式的值。根據題意,顯然 \(j\) 的位置越靠右越優。

那麼,若是有 \(j<j'\)\(g(j)>g(j')\)\(j'\) 必定比 \(j\) 優,\(j\) 就是沒用的了。

到這裏,優化方式已經呼之欲出——單調隊列!樸素想法就是在這個 \(j\) 單增 \(g(j)\) 單增的隊列裏面進行二分。可是這樣還有一個 \(\log\) .

再考慮左式 \(sum[i]\) 的單調遞增性質, 發現若是有一個點 \(j\) 對當前點 \(i\) 已經合法,能夠進行轉移了,那麼 \(j\) 以前的點雖然能用,可是顯然沒有 \(j\) 好用,就能夠丟掉了。因此每次從隊頭彈出直到留下最後一個合法點便可。

每一個點只會入隊一次出隊一次,均攤一下,轉移複雜度就是 \(\mathcal{O}(1)\) 的,總複雜度 \(\mathcal{O}(n)\) . 數據範圍誠不欺我

LOJ AC連接 給你們講個笑話,這道題我同一份代碼(去掉文件頭了)在 ACWing 上重複提交四次能獲得1次RE的好成績(

卡空間就有點過度,不過考慮到 OJ 確實開不起這麼大的空間也能夠理解,就是出題人太噁心。(包括這個 __int128 的離譜操做)

//Author: RingweEH
const int N=4e7+10,M=1e5+10;
int n,las[N],p[M],l[M],r[M],typ,que[N];
ll a[N];

ll g( int x )
{
    return a[x]*2-a[las[x]];
}

int main()
{
//freopen( "partition.in","r",stdin ); freopen( "partition.out","w",stdout );

    n=read(); typ=read();
    if ( typ==0 )
    {
		for ( int i=1; i<=n; i++ )
			scanf( "%lld",&a[i] );
    }
	else
	{
		ll x,y,z; scanf( "%lld%lld%lld",&x,&y,&z );
		int now=0,b[2],m; scanf( "%d%d%d",&b[0],&b[1],&m );
		for ( int i=1; i<=m; i++ )
			scanf( "%d%d%d",&p[i],&l[i],&r[i] );
		for ( int i=1; i<=n; i++ )
		{
			while ( p[now]<i ) now++;
			if ( i<=2 ) a[i]=b[i-1]%(r[now]-l[now]+1)+l[now];
			else
			{
				b[0]^=b[1]^=(b[0]=(y*b[0]+x*b[1]+z)%(1<<30))^=b[1];
				a[i]=b[1]%(r[now]-l[now]+1)+l[now];
			}
		}
	}

    for ( int i=1; i<=n; i++ )
        a[i]+=a[i-1];
    int l=0,r=0;
    for ( int i=1; i<=n; i++ )
    {
        while ( l<r && g(que[l+1])<=a[i] ) l++;
        las[i]=que[l];
        while ( l<r && g(que[r])>=g(i) ) r--;
        que[++r]=i; 
    }

	I128 ans=0;
	while ( n ) ans+=(I128)(a[n]-a[las[n]])*(a[n]-a[las[n]]),n=las[n];
	int cnt=0;
	do
	{
		que[++cnt]=ans%10; ans/=10;
	}while ( ans );
	do
	{
		printf( "%d",que[cnt] ); cnt--;
	}while ( cnt );

//fclose( stdin ); fclose( stdout );
//    return 0;
}

Day2-T3 樹的重心(centroid)

給定一棵 \(n\) 點的樹,求單獨刪去每條邊以後,分裂出的兩個子樹的重心編號和之和。(重心定義和簡單性質自行閱讀題面)

\(n\leq 299995\) .

Thoughts & Solution

55pts 有手就行

考場騙分小能手狂喜(

發現前面 40 分的部分分徹底能夠 \(\mathcal{O}(n^2)\) 暴力碾過去,枚舉刪邊,而後 \(\mathcal{O}(n)\) DFS求一遍重心便可。

對於後面 15 分,有性質 \(A\) 也就是鏈。對於鏈,重心顯然是找個中點就行了。

75pts 徹底二叉樹

咳……這個要面向數據。

注意到題目裏面對於這個部分分,欽定了 \(n=262143\) ,算一算就會發現是個滿二叉樹……其實滿二叉樹的根節點就是重心……

那麼能夠獲得以下推論:

  • 對於刪掉的某一條邊,兒子節點就是它這個子樹的重心
  • 對於根節點,若是在左子樹裏面刪了一條邊,那麼右兒子就是剩餘部分的重心
  • 對於葉子節點,刪掉以後根就是剩餘部分的重心

而後直接 \(\mathcal{O}(n)\) 枚舉 \(\mathcal{O}(1)\) 計算就行了。

正解

考慮重心的出現位置。有結論:

對於一個節點 \(u\) ,若是 \(n-siz[u]\leq \lfloor n/2\rfloor\) ,且 \(u\) 自己並不是重心,那麼重心必定在 \(u\) 的重兒子裏面。

這個挺顯然的的吧。

而後就有一些顯然的推論:(此處的 \(u\) 依然知足 \(n-siz[u]\leq\lfloor n/2\rfloor\)

  • 前置:顯然 \(u\) 只有一個重兒子。
  • 重心的可能位置只有兩種,要麼是 \(u\) 要麼在 \(u\) 的重子樹裏面。
  • 若是 \(u\) 是知足這個條件且 \(dep[u]\) 最大的點,那麼根據上面的結論, \(u\) 就是重心,且 \(fa[u]\) 也有多是重心。

所以,重心必定在 root 向下的重鏈上,並且重鏈上自上往下,節點的 \(siz\) 遞減。再結合數據範圍獲得合理猜想:複雜度 \(\mathcal{O}(n\log n)\) .

那麼就能夠考慮在重鏈上倍增。令 \(f[i][x]\) 表示以 rt 爲根,節點 \(x\) 沿着重鏈往下走 \(2^i\) 步達到的節點。這樣,求重心的時候就相似 LCA 同樣,逆序枚舉 \(i\) 往下跳就行了。

而後相似換根DP,二次掃描維護 \(f\) 數組和重兒子便可。

時間複雜度是 \(\mathcal{O}(n\log n)\) .

//Author: RingweEH
const int N=3e5+10,K=25;
struct edge
{
    int to,nxt;
}e[N<<1];
int head[N],tot=0,n,siz[N],f[N][K],son[N],fa[N];
ll ans;

void ST_init( int x )
{
    for ( int i=1; i<K; i++ )
        f[x][i]=f[f[x][i-1]][i-1];
}

void calc( int x )
{
    int u=x;
    for ( int i=K-2; i>=0; i-- )
        if ( f[u][i] && siz[f[u][i]]*2>=siz[x] ) u=f[u][i];
    if ( siz[u]*2==siz[x] ) ans+=fa[u];
    ans+=u;
}

void dfs( int u,int fat )
{
    fa[u]=fat; siz[u]=1; siz[0]=0; son[u]=0;
    for ( int i=head[u]; i; i=e[i].nxt )
    {
        int v=e[i].to;
        if ( v==fat ) continue;
        dfs( v,u ); siz[u]+=siz[v];
        if ( siz[v]>siz[son[u]] ) son[u]=v; //重兒子
    }
    f[u][0]=son[u]; ST_init( u );
}

void get_ans( int u,int fat )
{
    int mx1=0,mx2=0; siz[0]=0;
    for ( int i=head[u]; i; i=e[i].nxt )
    {
        int v=e[i].to;
        if ( siz[v]>=siz[mx1] ) mx2=mx1,mx1=v;
        else if ( siz[v]>=siz[mx2] ) mx2=v;
        //最大和次大的兒子
    }
    for ( int i=head[u]; i; i=e[i].nxt )
    {
        int v=e[i].to;
        if ( v==fat ) continue;
        calc( v ); f[u][0]=(v==mx1) ? mx2 : mx1; ST_init( u );
        siz[u]-=siz[v]; siz[v]+=siz[u];
        calc( u ); fa[u]=v; get_ans( v,u );
        siz[v]-=siz[u]; siz[u]+=siz[v];     //算完(u,v)以後撤銷影響
    }
    f[u][0]=son[u]; ST_init( u ); fa[u]=fat;
}

void add( int u,int v )
{
    e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot;
    e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot;
}

int main()
{
//freopen( "centroid.in","r",stdin ); freopen( "centroid.out","w",stdout );

    int T=read();
    while ( T-- )
    {
        memset( siz,0,sizeof(siz) ); memset( son,0,sizeof(son) );
        memset( f,0,sizeof(f) ); memset( fa,0,sizeof(fa) ); ans=0;
        memset( head,0,sizeof(head) ); tot=0;

        n=read();
        for ( int i=1,u,v; i<n; i++ )
            u=read(),v=read(),add( u,v );
        
        dfs( 1,0 ); get_ans( 1,0 );

        printf( "%lld\n",ans );
    }

//fclose( stdin ); fclose( stdout );
    return 0;
}
相關文章
相關標籤/搜索