【UOJ】Round48:Goodbye Wuxu题解(A-C)

传送门:
Goodbye Wuxu
题解
D,E暂咕


A. 新年的拯救计划

一共 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1)条边,所以 m ≤ ⌊ n 2 ⌋ m\leq \lfloor \frac {n}{2}\rfloor m2n

上限是一定可以卡满的:考虑构造 ⌊ n 2 ⌋ \lfloor \frac {n}{2}\rfloor 2n条起终点均不同的链,分奇偶讨论。
(然后就想歪了,还是没想到怎么 n 2 n^2 n2复杂度以内构造这么多条链)

一档部分分叫“ n n n是素数”:

可以联想到对于整数 1 ≤ k &lt; n 1\leq k&lt;n 1k<n gcd ⁡ ( k , n − k ) = 1 \gcd(k,n-k)=1 gcd(k,nk)=1

由于 n n n必然为奇数( n ≥ 3 n\geq 3 n3),所以可以构造 ⌊ n 2 ⌋ \lfloor \frac {n}{2}\rfloor 2n棵树,对于第 i ( 1 ≤ i ≤ ⌊ n 2 ⌋ ) i(1\leq i\leq \lfloor \frac {n}{2}\rfloor) i(1i2n)棵树,将所有 ∣ u − v ∣ = i |u-v|=i uv=i ∣ u − v ∣ = n − i |u-v|=n-i uv=ni连边(连出来是个环,随便断一条边即可)。

再考虑 n n n为偶数的情况:

假设已经构造出了第一棵树 T 0 T_0 T0
对于每条边 ( u , v ) (u,v) (u,v),在第 1 ≤ i &lt; n 2 1\leq i&lt;\frac n2 1i<2n棵树中连接 ( ( u + i )   m o d   n , ( v + i )   m o d   n ) ((u+i) \ mod\ n,(v+i)\ mod\ n) ((u+i) mod n,(v+i) mod n)

对于一条边 ( u , v ) ( u &lt; v ) (u,v)(u&lt;v) (u,v)(u<v),定义其长度为 min ⁡ ( v − u , u − v + n ) \min(v-u,u-v+n) min(vu,uv+n),考虑每条边不断循环右移 [ 0 , n 2 ) [0,\frac n2) [0,2n)中长度变化的过程:

长度相同且 &lt; n 2 &lt;\frac n2 <2n的边最多可以存在两条: ( u , v ) (u,v) (u,v) ( ( u + n 2 )   m o d   n , ( v + n 2 )   m o d   n ) ((u+\frac n2) \ mod \ n,(v+\frac n2 )\ mod \ n) ((u+2n) mod n,(v+2n) mod n)
长度为 n 2 \frac n2 2n的边只能存在一条。

所以对于 T 0 T_0 T0,构造 2 × ( n 2 − 1 ) + 1 = n − 1 2\times (\frac n2-1)+1=n-1 2×(2n1)+1=n1条边,下面是一种例子:
1 1 1连向 [ 2 , n 2 + 1 ] [2,\frac n2 +1] [2,2n+1]之间的所有点, n 2 + 1 \frac n2+1 2n+1连向 [ n 2 + 2 , n ] [\frac n2+2,n] [2n+2,n]之间的所有点。

n n n为奇数时范围反而变宽了(长度为 ⌊ n 2 ⌋ \lfloor \frac n2\rfloor 2n的边也能存在两条),取同样的构造方法即可。

(A题都做不出来,给跪了)


B. 新年的Dog划分

假设二分图左右点集分别为 S , T S,T S,T

首先考虑怎么暴力:

2 n 2^n 2n枚举左右点集合 A , B A,B A,B,询问时只保留 A B AB AB之间的连边,若是二分图,则必然存在一种情况的答案是连通:

假设 A ∩ S ≠ ϕ A\cap S\neq \phi AS̸=ϕ
A ∩ T ≠ ϕ A\cap T\neq \phi AT̸=ϕ,则 A ∩ S A\cap S AS A ∩ T A\cap T AT不连通(它们分别只能和 B ∩ T B\cap T BT B ∩ S B\cap S BS相连),矛盾,所以 A ∩ T = ϕ A\cap T=\phi AT=ϕ
B ∩ S ≠ ϕ B\cap S\neq \phi BS̸=ϕ,则 A ∩ S A\cap S AS B ∩ S B\cap S BS不连通,矛盾,所以 B ∩ S = ϕ B\cap S=\phi BS=ϕ

所以 A = S , B = T A=S,B=T A=S,B=T

正解:

只需要找到原图的一个生成树:

1 1 1开始向外 B F S BFS BFS维护当前连通块。
每次取出队顶元素 x x x,不断在所有未加入连通块的点中二分是否存在最小的 y y y,使得删掉连通块与这个点之间的连边之后图不连通, ( x , y ) (x,y) (x,y)则是生成树上的一条边,询问次数上限 O ( n log ⁡ n ) O(n\log n) O(nlogn)

构造生成树后黑白点染色得到二分图,还需要判断是否是二分图:
删去二分图左右部之间的所有连边(生成树上的边除外),若不是二分图,则必然在树上有环,则枚举删掉一条树边判断图是否连通即可。

在提交记录里翻到一个十分优秀简洁的写法:

每次直接维护当前连通块中二分图左右部的点,二分找到不在连通块内且删去该点与连通块连边后图不连通的点 y y y,分别判断删去左部/右部与 y y y连边后是否连通,若两边删去均连通,则不是二分图,否则就找到了 y y y所在部分(左/右)。

#include <bits/stdc++.h>
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
using namespace std;
#include "graph.h"
const int N=205;
int col[N];
map<int,bool> result;

vector<int> check_bipartite(int n)
{
	int i,j,k,l,r,mid,res,t;
	bool isb=true;col[0]=1;
	for(i=1;i<n;++i){
		vector<int>s;
		for(j=1;j<n;++j) if(!col[j]) s.pb(j);
		l=0,r=s.size()-1;res=s.size();
		for(;l<=r;){
			mid=(l+r)>>1;
			vector<pair<int,int> > le;
			for(j=mid;j<s.size();++j)
				for(k=0;k<n;++k) if(col[k]) le.pb(mkp(s[j],k));
			query(le)?r=mid-1:l=(res=mid)+1;
		}
		result[-1]=result[1]=false;
		for(t=-1;t<=1;t+=2){
			vector<pair<int,int> > le;
			for(j=res+1;j<s.size();++j)
				for(k=0;k<n;++k) if(col[k])le.pb(mkp(s[j],k));
			for(k=0;k<n;++k) if(col[k]==t)le.pb(mkp(s[res],k));
			result[t]=query(le);
		}
		if(result[-1]&&result[1]){isb=false;break;}
		col[s[res]]=result[1]?1:-1;
	}
	vector<int> ans;
	if(isb)
		for(i=0;i<n;++i)
			if(col[i]>0) ans.push_back(i);
	return ans;
}

C. 新年的小黄鸭

35 p t s 35pts 35pts n 2 n^2 n2暴力:

f [ i ] [ j ] f[i][j] f[i][j]表示 i i i点向上的连续重链长度为 j j j时, i i i子树内的最优解。
转移时直接枚举重儿子。

#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=5005;

int lg[N],n,sz[N],f[N][N];
vector<int>g[N];

inline void dn(int &x,int y){if(y<x) x=y;}
void dfs(int x,int fr)
{
	int i,sum=0;sz[x]=1;
	for(int y:g[x]) if(y^fr){
		dfs(y,x);sz[x]+=sz[y];sum+=f[y][0];
		for(i=0;i+1<n;++i) dn(f[x][i],f[y][i+1]-f[y][0]-(i?sz[y]*lg[i]:0));
	}
	for(i=0;i<n;++i) f[x][i]+=(sum+sz[x]*lg[i]);
}

int main(){
	int i,x,y;lg[0]=1;
	scanf("%d",&n);
	for(i=1;i<n;i<<=1) lg[i+1]=1;
	for(i=1;i<n;++i) lg[i]+=lg[i-1];
	for(i=1;i<n;++i){scanf("%d%d",&x,&y);g[x].pb(y);g[y].pb(x);}
	dfs(1,0);printf("%d",f[1][0]-n);
	return 0;
}

正解:

t r a n s ( x ) = ⌈ log ⁡ 2 x ⌉ + 1 trans(x)=\lceil\log_2 x \rceil+1 trans(x)=log2x+1

观察到 f [ i ] [ j ] f[i][j] f[i][j]中对于 t r a n s ( j ) trans(j) trans(j)相等的 j j j其实可以用同一个 f [ i ] [ k ] f[i][k] f[i][k]来表示,所以只需要记 f [ i ] [ t r a n s ( j ) ] f[i][trans(j)] f[i][trans(j)]的值,状态就变成 n log ⁡ n n\log n nlogn的了。

考虑转移:
对于 f [ i ] [ j ] f[i][j] f[i][j],枚举 i i i重链上(子树内)第一个满足 j j j的值不同或是叶结点的点,设其为 y y y,则 f [ i ] [ j ] = f [ y ] [ j + 1 ] + f [ y 到 i 路 径 上 所 有 轻 儿 子 ] [ 0 ] + ( y 到 i 路 径 上 除 y 外 所 有 点 的 值 ) f[i][j]=f[y][j+1]+f[y到i路径上所有轻儿子][0]+(y到i路径上除y外所有点的值) f[i][j]=f[y][j+1]+f[yi][0]+(yiy)

……

还是比较玄学,算了,直接讲做法吧:

首先预处理出来每个点可以由它子树内哪些点转移而来(所有距离 = 1 =1 =1 2 i + 1 2^i+1 2i+1),记为 n t [ x ] nt[x] nt[x]数组,总数量级别是 O ( n log ⁡ n ) O(n\log n) O(nlogn)的,且按 d f s dfs dfs序给每个叶结点标号, [ i n x , o u t x ] [in_x,out_x] [inx,outx]表示在子树 x x x中的叶节点范围。

d p [ x ] = f [ x ] [ 0 ] − s z [ x ] dp[x]=f[x][0]-sz[x] dp[x]=f[x][0]sz[x](即 x x x为根的答案,叶结点的 d p dp dp值为 0 0 0)。
对于 d p [ x ] dp[x] dp[x],本质上是选中一个子树内叶结点 y y y x x x y y y这条路径就是重链。 d p [ x ] = m i n ( v ( x 到 y 路 径 上 所 有 点 ) ) ( i n x ≤ y ≤ o u t x ) dp[x]=min(v(x到y路径上所有点))(in_x\leq y\leq out_x) dp[x]=min(v(xy))(inxyoutx)

v ( y ) = t r a n s ( d i s ( x , y ) ) + ( ∑ t ∈ s o n [ f a t h e r y ] d p [ t ] + s z [ t ] ( 1 + t r a n s ( d i s ( x , t ) ) ) ) − d p [ y ] − s z [ y ] ( 1 + t r a n s ( d i s ( x , y ) ) ) = ( ∑ t ∈ s o n [ f a t h e r y ] d p [ t ] + s z [ t ] ) − d p [ y ] − s z [ y ] + ( s z [ f a t h e r y ] − s z [ y ] ) t r a n s ( d i s ( x , y ) ) v(y)=trans(dis(x,y))+(\sum \limits_{t\in son[father_y]}dp[t]+sz[t](1+trans(dis(x,t))))-dp[y]-sz[y](1+trans(dis(x,y))) \\ \qquad =(\sum \limits_{t\in son[father_y]}dp[t]+sz[t])-dp[y]-sz[y]+(sz[father_y]-sz[y])trans(dis(x,y)) v(y)=trans(dis(x,y))+(tson[fathery]dp[t]+sz[t](1+trans(dis(x,t))))dp[y]sz[y](1+trans(dis(x,y)))=(tson[fathery]dp[t]+sz[t])dp[y]sz[y]+(sz[fathery]sz[y])trans(dis(x,y))

对于前一部分: ∑ ( ( ∑ t ∈ s o n [ f a t h e r y ] d p [ t ] + s z [ t ] ) − d p [ y ] − s z [ y ] ) \sum((\sum \limits_{t\in son[father_y]}dp[t]+sz[t])-dp[y]-sz[y]) ((tson[fathery]dp[t]+sz[t])dp[y]sz[y])直接在线段树上按关于叶结点的 d f s dfs dfs序维护即可。

对于后一部分:实际上就是 y y y和所有轻儿子子树这些点到根的贡献为 t r a n s ( x , y ) trans(x,y) trans(x,y)
y y y子树中的重链部分的贡献还要另算,轻链部分子树中的 v ( ) v() v()会算到。
但注意 t r a n s ( x , y ) trans(x,y) trans(x,y)只有 log ⁡ n \log n logn中取值,我们把将一个点 × t r a n s ( x , y ) \times trans(x,y) ×trans(x,y)转成对于每个点的在它的 1 , 2 0 + 1 , 2 1 + 1 , . . . , 2 k + 1 1,2^0+1,2^1+1,...,2^k+1 1,20+1,21+1,...,2k+1级祖先处分别被算一次。
也就是线段树中将 n t [ x ] nt[x] nt[x]中所有点 q q q在其对应的范围 [ i n q , o u t q ] [in_q,out_q] [inq,outq] + s z [ q ] +sz[q] +sz[q]

d p [ x ] dp[x] dp[x]就是查询线段树 [ i n x , o u t x ] [in_x,out_x] [inx,outx]内最小值。

面向代码做题(大雾)

#include<bits/stdc++.h>
#define lc k<<1
#define rc k<<1|1
#define mid ((l+r)>>1)
#define pb push_back
using namespace std;
const int N=1e5+10;

int n,f[N][17],in[N],ot[N],sz[N],dfn,dp[N];
int mn[N<<2],lzy[N<<2];
vector<int>g[N],nt[N];

inline void ad(int k,int l,int r,int L,int R,int v)
{
	if(L<=l && r<=R) {mn[k]+=v;lzy[k]+=v;return;}
	if(L<=mid) ad(lc,l,mid,L,R,v);
	if(R>mid) ad(rc,mid+1,r,L,R,v);
	mn[k]=min(mn[lc],mn[rc])+lzy[k];
}

inline int ask(int k,int l,int r,int L,int R)
{
	if(L<=l && r<=R) return mn[k];
	if(R<=mid) return ask(lc,l,mid,L,R);
	if(L>mid) return ask(rc,mid+1,r,L,R);
	return min(ask(lc,l,mid,L,R),ask(rc,mid+1,r,L,R))+lzy[k];
}

void dfs1(int x,int fr)
{
	int i;f[x][0]=fr;if(fr) nt[fr].pb(x);
	for(i=1;i<17;++i) f[x][i]=f[f[x][i-1]][i-1];
	for(i=0;i<17;++i) if(f[f[x][i]][0]) nt[f[f[x][i]][0]].pb(x);
	sz[x]=1;in[x]=dfn+1;if(g[x].size()==1 && fr) dfn++;
	for(int y:g[x]) if(y^fr) {dfs1(y,x);sz[x]+=sz[y];}
	ot[x]=dfn;
}

void dfs2(int x,int fr)
{
	int sum=0;
	for(int y:g[x]) if(y^fr) {dfs2(y,x);sum+=dp[y]+sz[y];}
	for(int y:g[x]) if(y^fr) ad(1,1,dfn,in[y],ot[y],sum-dp[y]-sz[y]);
	for(int y:nt[x]) ad(1,1,dfn,in[y],ot[y],sz[y]);
	dp[x]=ask(1,1,dfn,in[x],ot[x]);
	for(int y:nt[x]) ad(1,1,dfn,in[y],ot[y],-sz[y]);
}

int main(){
	int i,x,y;scanf("%d",&n);
	for(i=1;i<n;++i) {scanf("%d%d",&x,&y);g[x].pb(y);g[y].pb(x);}
	dfs1(1,0);dfs2(1,0);printf("%d",dp[1]);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值