CSP-S 2019 Solution
Day1-T1 格雷码(code)
格雷码是一种特殊的 \(n\) 位二进制串排列法,要求相邻的两个二进制串恰好有一位不同,环状相邻。
生成方法:
- \(1\) 位格雷码由两个 \(1\) 位的二进制串组成,顺序为 \(0,1\)
- \(n+1\) 位的格雷码的前 \(2^n\) 个串,是由 \(n\) 位格雷码顺序排列再加前缀 0 组成。
- 后 \(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}\) 那么有:
\]
发现 取了 1 个的方案
其实可以直接在 存在一列取了超过半数的方案
里面统计掉,因为一定是超过半数的。
没有取的方案直接不加上就好了。
然后就可以暴力枚举超过半数的材料是哪个,进行DP。
设 \(f[i][j][k]\) 表示前 \(i\) 行,取了 \(j\) 个,其中超过半数的 \(x\) 取了 \(k\) 个( \(\Big\lfloor \dfrac{j}{2}\Big\rfloor <k\)),枚举到 \(pos\) 这道菜取了超过半数。
转移挺好想的,就是三种情况:
- 不取
- 取了除 \(pos\) 外的任意一个
- 取了 \(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\) ,省掉一维的枚举时间和空间。
那么方程就是:
\]
(数组下标内的小括号表示根据原先的 \(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\) ,使得:
\]
\(p\) 可以为 \(0\) 且此时 \(k_0=0\) .然后要求最小化::
\]
求这个最小的值。
(数据生成方式见题面)
\(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]\) 为序列前缀和。
有方程:
\]
复杂度为 \(\mathcal{O}(n^3)\) .问题出在上一个断点要一个一个枚举 \(k\) 得到。考虑如何加速这个过程。
注意到 “平方之和”一定比 “和的平方”要小。所以把最后一段拆成几段(在满足递增的情况下)答案一定不会变劣。
也就是说最优解的方案一定是合法的里面 最后一段最短 的一种。
那么这时候的 \(k\) 就是确定的,数组就省掉了一维变成 \(f[i]\) .记录一个 \(las[i]\) 表示 \(f[i]\) 的方案中上一段的末尾。
方程就是:
j 满足 sum[j]-sum[las[j]]\leq sum[i]-sum[j].
\]
复杂度 \(\mathcal{O}(n^2)\) 。这样已经实现了36分到64分的巨大飞跃(
然而对于 \(n\leq 4e7\) ,加上常数的话复杂度得是线性的……继续优化。
注意上面的 \(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的更多相关文章
- 上午小测3 T1 括号序列 && luogu P5658 [CSP/S 2019 D1T2] 括号树 题解
前 言: 一直很想写这道括号树..毕竟是在去年折磨了我4个小时的题.... 上午小测3 T1 括号序列 前言: 原来这题是个dp啊...这几天出了好几道dp,我都没看出来,我竟然折磨菜. 考试的时候先 ...
- @CSP模拟2019.10.16 - T3@ 垃圾分类
目录 @description@ @solution@ @accepted code@ @details@ @description@ 为了保护环境,p6pou建设了一个垃圾分类器. 垃圾分类器是一个 ...
- Hello 2019 Solution
A. Gennady and a Card Game 签到. #include <bits/stdc++.h> using namespace std; ], t[]; bool solv ...
- AISing Programming Contest 2019 Solution
A - Bulletin Board 签到. #include <bits/stdc++.h> using namespace std; int main() { int n, h, w; ...
- KEYENCE Programming Contest 2019 Solution
A - Beginning 签到. #include <bits/stdc++.h> using namespace std; int main() { ]; while (scanf(& ...
- CSP/NOIP 2019 游记
Day0 打牌 Day1 \(T1\) 没开\(ull\), 不知道有几分 \(T2\) \(N^2\)暴力+链, 没搞出树上做法, \(70\)分 \(T3\) 标准\(10\)分( 感觉今年省一稳 ...
- 【置顶】CSP/S 2019退役祭
标题没错,今年就是我的最后一年了. 才高一啊,真不甘心啊. DAY1(之前的看前几篇博客吧) T1 现在没挂 T2 貌似是树形DP,跑到80000的深度时挂了,于是特判了链的情况,大样例过了,现在没挂 ...
- Cisco Common Service Platform Collector - Hardcoded Credentials(CVE-2019-1723)
Cisco Common Service Platform Collector - Hardcoded Credentials 思科公共服务平台收集器-硬编码凭证(CVE-2019-1723) htt ...
- CSP-S2019 游记
想到正解,不一定赢 全部打满,才是成功 Day 0 首先很感谢各位朋友送的贺卡!!! 早上10点的高铁.今年可以直接在汕头站坐高铁不用专门跑到潮汕站了,1h->15min车程,巨大好评. 虽然离 ...
随机推荐
- linux netfilter rule match target 数据结构
对于netfilter 可以参考 https://netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html netfilter ...
- JAVA 去除字符串前后的指定字符
为了显示效果更好,我们可以将多余的字符去掉,代码如下: /** *去掉字符串前后的指定字符 */ public static String trimBothChars(String str, Stri ...
- rocketmq详解-[个人版]-第一章
一.消息队列概述 1.1.消息队列由来 在运维场景中,我们经常会存在如下场景:一旦出现S1异常,C1将因为S1的影响而异常(C为客户端,s为服务端) 当然可以通过添加多个S的方式,实现高可用.但这样会 ...
- kali 系列学习12-使用Wifite破解无线网络
一些破解无线网络程序是使用Aircrack-ng工具集,并添加了一个图形界面或使用文本菜单的形式来破解无线网络.这使得用户使用它们更容易,而且不需要记住任何命令.本节将介绍使用命令行工具Wifite, ...
- 学好Flex布局并不容易
1. Flex布局介绍 CSS的传统布局解决方案,基于盒状模型,依赖display属性.position属性.float属性,对于一些特殊的布局,例如垂直居中,往往要想很多hack的方法来解决. 20 ...
- FL Studio通道乐器设置页详讲
上一篇文章我们说到FL Studio通道乐器设置页每个标签页面中几乎都是由包络.低频振荡器和滤波器这三个部分组成.我们之前只对包络进行的简单的介绍,相信很多同学对它还有其他两个的功能的了解还是云里雾里 ...
- FL Studio12如何进行图示编辑
FL Studio在国内被大家 亲切的称为"水果"深受喜爱玩电音的音乐人的追捧,本章节采用图文结合的方式给大家讲解它的FL Studio12是如何进行图示编辑的. 单击图示按钮可以 ...
- web自动化测试难点 滚动条操作、日期框处理、上传文件
如何把页面滑到最低部? 一般来说,做web自动化测试时,不需要单独写代码,把页面滑到可见,因为click操作,只要是元素存在并且加载出来了,就可以点击到,无需另外写滑动元素的代码. 如果特殊情况需要滑 ...
- HDU 4920 Matrix multiplication 题解(内存访问连续性/卡常)
题目链接 题目大意 多组输入,给你两个n×n的矩阵,要你求他们相乘%3的值 题目思路 这个题目主要是要了解内存访问连续化,要尽量每次访问连续的内存 所以第一种方法会超时,第二种则AC.一种卡常技巧 代 ...
- Snap Build Your Own Block修炼之道-添加自定义类别
Snap Build Your Own Block自我修炼方法:1.所有的面向对象,其实是对面向过程的抽象过程而已: 2.面对别人的开源项目时,需要找准源头(即项目运行的起点,当然有的是没有的哈,没有 ...