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;
}

CSP-S 2019 Solution的更多相关文章

  1. 上午小测3 T1 括号序列 && luogu P5658 [CSP/S 2019 D1T2] 括号树 题解

    前 言: 一直很想写这道括号树..毕竟是在去年折磨了我4个小时的题.... 上午小测3 T1 括号序列 前言: 原来这题是个dp啊...这几天出了好几道dp,我都没看出来,我竟然折磨菜. 考试的时候先 ...

  2. @CSP模拟2019.10.16 - T3@ 垃圾分类

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 为了保护环境,p6pou建设了一个垃圾分类器. 垃圾分类器是一个 ...

  3. Hello 2019 Solution

    A. Gennady and a Card Game 签到. #include <bits/stdc++.h> using namespace std; ], t[]; bool solv ...

  4. AISing Programming Contest 2019 Solution

    A - Bulletin Board 签到. #include <bits/stdc++.h> using namespace std; int main() { int n, h, w; ...

  5. KEYENCE Programming Contest 2019 Solution

    A - Beginning 签到. #include <bits/stdc++.h> using namespace std; int main() { ]; while (scanf(& ...

  6. CSP/NOIP 2019 游记

    Day0 打牌 Day1 \(T1\) 没开\(ull\), 不知道有几分 \(T2\) \(N^2\)暴力+链, 没搞出树上做法, \(70\)分 \(T3\) 标准\(10\)分( 感觉今年省一稳 ...

  7. 【置顶】CSP/S 2019退役祭

    标题没错,今年就是我的最后一年了. 才高一啊,真不甘心啊. DAY1(之前的看前几篇博客吧) T1 现在没挂 T2 貌似是树形DP,跑到80000的深度时挂了,于是特判了链的情况,大样例过了,现在没挂 ...

  8. Cisco Common Service Platform Collector - Hardcoded Credentials(CVE-2019-1723)

    Cisco Common Service Platform Collector - Hardcoded Credentials 思科公共服务平台收集器-硬编码凭证(CVE-2019-1723) htt ...

  9. CSP-S2019 游记

    想到正解,不一定赢 全部打满,才是成功 Day 0 首先很感谢各位朋友送的贺卡!!! 早上10点的高铁.今年可以直接在汕头站坐高铁不用专门跑到潮汕站了,1h->15min车程,巨大好评. 虽然离 ...

随机推荐

  1. Centos7上一次War包的部署与运行

    Centos7上一次War包的部署与运行 前言 由于前段时间第一次部署一个小型的项目,时间一长所以有些步骤有时候时间一长就忘了,在此做个简单的记录 一.原始系统开发环境 操作系统:Windows10: ...

  2. nginx&http 第二章 ngx启动多进程

    Nginx服务器使用 master/worker 多进程模式. 主进程(Master process)启动后,会接收和处理外部信号: 主进程启动后通过fork() 函数产生一个或多个子进程(work ...

  3. Socket 结构体

    proto socket 关联结构: { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &am ...

  4. MySQL慢查询开启、日志分析(转)

    说明 Mysql的查询讯日志是Mysql提供的一种日志记录,它用来记录在Mysql中响应时间超过阈值的语句 具体指运行时间超过long_query_time值得SQL,则会被记录到慢查询日志中.lon ...

  5. cephonebox发布

    前言 现在已经是2016年收官的一个月了,之前一直想做一个calamari的集成版本,之所以有这个想法,是因为,即使在已经打好包的情况下,因为各种软件版本的原因,造成很多人无法配置成功,calamar ...

  6. Kubernetes 入门与安装部署

    一.简介 参考:Kubernetes 官方文档.Kubernetes中文社区 | 中文文档 Kubernetes 是一个可移植的.可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自 ...

  7. 新建Chrome标签页,极简+自用

    [跳转GitHub] chromeNewTab 已经入坑Chrome应用开发者,可以去:[应用商店地址]直接添加使用. 使用说明 下载chrome的一个[window组策略文件],解压文件后找到(\p ...

  8. 对Tarjan——有向图缩点算法的理解

    开始学tarjan的时候,有关无向图的割点.桥.点双边双缩点都比较容易地理解了,唯独对有向图的缩点操作不甚明了.通过对luoguP2656_采蘑菇一题的解决,大致搞清了tarjan算法的正确性. 首先 ...

  9. JavaSE 学习笔记08丨网络编程

    Chapter 14. 网络编程 14.1 计算机网络入门 当前节的知识点只是一个概述,更具体.详细的内容放在 计算机网络 中. 14.1.1 软件结构 C/S结构(Client/Server结构): ...

  10. 精尽MyBatis源码分析 - 文章导读

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...