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;
}
相关文章
相关标签/搜索