CSP-S 2019 Solution

Day1-T1 格雷码(code)

格雷码是一种特殊的 \(n\) 位二进制串排列法,要求相邻的两个二进制串恰好有一位不同,环状相邻。

生成方法:

  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

考虑一个跟康托展开非常相似的思路。

首先看第一位,如果是 1 那么说明它前面已经可以确定至少排了 \(2^n\) 个 0 开头的二进制串。

那么这样就可以确定第一位是 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}\)

写代码的时候注意溢出问题,要开 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;
}
posted @ 2020-12-03 21:27  MontesquieuE  阅读(381)  评论(0编辑  收藏  举报