MX 暑假集训 7.14

【25時、ナイトコードで。×MEIKO】アイディスマイル(IDSMILE)

哇这个模拟赛太符合我的品味了,打的最好的一集。

T1: 最小割

题意

给定一棵 \(n\) 个节点的树,令 \(f(S)\) 为割掉若干条边,使得集合 \(S\) 内的点两两不连通的割掉边权和的最小值。

给定一个长度为 \(n\) 的排列 \(p\),初始时 $S=\emptyset $,接下来向 \(S\) 依次加入 \(n\) 个点,第 \(i\) 次加入 \(p_i\),你需要输出每次加入完点后 \(f(S)\) 的值。

\(1\le T\le 5,1\le n\le 10^6\)

solution

何意味,不到一小时秒了,正解比暴力好写说是,感觉是最近模拟赛最简单 T1。

一条边加入割集后不会被移出割集,考场上可以感性理解,题解给出的证明是:考虑将 \(S\) 内的每个点向一个虚点 \(0\) 连边,边权为极大值,原条件等价于你要割掉一些边,使得剩下的边构成一棵树,跑一遍最大生成树,那么删掉的边一定是非树边,考虑 Kruskal 跑最大生成树的过程,我们不难发现一条边加入割集后不会被移出割集。

那么就有一个暴力,每次加入一个点时,找到与它在同一个连通块内的已经加入 \(S\) 的点,将它们路径上边权最小的边割掉,正确性可以结合上面的 Kruskal 理解。

这个暴力是 \(O(n^2)\) 的,可以拿 LCT 做到 \(O(n\log n)\),但是常数又大又难写,考虑有没有更优秀的做法。

我们实际上在模拟一个删边的过程,于是我们倒过来想,就可以变成加边的过程,从而使用并查集高效维护。

按边权从大到小排序,一条边被加入割集的时间为:仅考虑边权比它大的边,对于两个端点,其连通块内出现时间最小值,这两个最小值的较大值。

用并查集维护即可,时间复杂度 \(O(n\log n)\),瓶颈在排序。

Code
#include<cstdio>
#include<algorithm>
#include<bitset>
#include<vector>
using namespace std;
#define qwq Ff472130
#define ll long long
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,r,l) for (int i=r;i>=l;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar();
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
ll s[N];

struct Node{int u,v,w;}e[N];
inline bool cmp(Node x,Node y) {return x.w>y.w;}

int f[N],mx[N];
inline int find(int x) {
	if (f[x]==x) return x;
	return f[x]=find(f[x]);
}

inline void solve() {
	read(n);
	f(i,1,n-1) {
		int u,v,w;
		read(u);read(v);read(w);
		e[i]={u,v,w};
	}
	sort(e+1,e+n,cmp);
	f(i,1,n) {
		int x;read(x);
		f[i]=i;mx[x]=i;
	}
	f(i,1,n-1) {
		int x=find(e[i].u),y=find(e[i].v);
		s[max(mx[x],mx[y])]+=e[i].w;
		f[x]=y;mx[y]=min(mx[y],mx[x]);
	}
	f(i,1,n) {
		s[i]+=s[i-1];
		printf("%lld ",s[i]);
	}
	putchar(10);
	f(i,1,n) s[i]=0;
}

int main() {
	freopen("mincut.in","r",stdin);
	freopen("mincut.out","w",stdout);
	int C,T;
	read(C);read(T);
	while (T--) solve();
	return 0;
}

T2:Directed Acyclic Graph

题意

给定一张 \(n\) 个节点 \(m\) 条边的有向无环图,第 \(i\) 个点初始时点权为 \(a_i\)\(q\) 次操作,操作有以下三种。

  • 给定 \(x,v\),对于所有 \(x\) 可达点 \(y\)\(a_y\leftarrow v\)

  • 给定 \(x,v\),将所有 \(x\) 可达点的点权都改为 \(a_y\leftarrow \min(a_y,v)\)

  • 给定 \(x\),输出 \(a_x\)

\(1\le n,m,q\le 2\times 10^5\)

solution

追忆加强版,离正解仅差一步之遥,不过部分分拿得很爽。

首先这个问题显然不弱于有向图可达性,时间复杂度不低于 \(O(\frac{n^2}{\omega })\),其次这个问题很像追忆,而且每个操作只涉及一个关键点,考虑操作分块,每 \(B\) 个操作为一块。

同时处理一个块内的修改和询问,那么把每个操作的 \(x\) 拿出来,标记为关键点,在这个块内,我们需要的仅仅是这些点的可达性,所以拉出来跑有向图可达性,令 \(vis_{i,j}\) 表示 \(i\) 是否可以到达 \(j\),其中 \(j\) 为关键点,所以这一部分时间复杂度 \(O(\frac{nB}{\omega })\)

首先是块内的维护,对于每个修改操作,暴力将关键点的权值进行修改即可,这部分时间复杂度是 \(O(B^2)\) 的。

然后再考虑整块的维护,我们发现不能像追忆那样处理完一个块内的操作后将整个块对全局的修改快速求出来,于是在询问时直接加入这个块前面所有块的贡献。

将每个操作按顺序赋上一个时间,我们发现若一个点 \(x\) 最后一次被覆盖的时间为 \(t_1\),那么一个时间为 \(t_2\) 的 chkmin 操作能对 \(x\) 生效当且仅当 \(t_2>t_1\),所以我们记录每个点 \(x\) 最后一次被覆盖的时间,然后发现这个块前合法的 chkmin 操作为一部分的散块和一些整块。

散块的贡献暴力 chkmin 即可,整块的贡献可以维护一个数组 \(mn_{i,j}\) 表示第 \(i\) 个块对第 \(j\) 个点 chkmin 的最小值,遍历每个整块取最小值即可。

每个块的询问结束后,更新每个点最后一次被覆盖的时间和这个块对应的 \(mn\) 数组是简单的,拓扑排序传递即可。

计算前面块的贡献时间复杂度为 \(O(B(B+\frac{n}{B}))\),维护这个块对后面的信息时间复杂度为 \(O(n)\)

总时间复杂度 \(O(\frac{n^2}{\omega }+nB+\frac{n^2}{B})\),空间复杂度 \(O(\frac{n^2}{B}+\frac{nB}{\omega })\),取 \(B=\sqrt n\) 可得时间复杂度为 \(O(\frac{n^2}{\omega }+n\sqrt n)\),空间复杂度为 \(O(n\sqrt n)\)

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int B=500,RB=B+10,LB=N/B+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,q;
int a[N];
vector<int> e[N];

struct ques{int op,x,v,tim;};
vector<ques> qs[LB];
bitset<RB> vis[N];
int mp[N],mx[N],id[N];
int mn[LB][N],tg2[N];
struct Tag{int v,t;}tg1[N];

inline void clear(int cnt) {
	f(i,1,n) e[i].clear(),mx[i]=0;
	f(i,1,cnt) qs[i].clear();
}

inline void work(vector<ques> &tmp,int idx) {
	vector<int> pt;
	for (ques k:tmp) pt.push_back(k.x);
	int ptcnt=0;
	f(i,1,n) {
		vis[i].reset();
		tg1[i]={0,0};
		tg2[i]=inf;
	}
	for (int k:pt) {
		mp[k]=++ptcnt;
		vis[k].set(ptcnt);
	}
	F(u,n,1) for (int v:e[u]) vis[u]|=vis[v];
	for (ques k:tmp) {
		int op=k.op,x=k.x,v=k.v,t=k.tim;
		id[t]=idx;
	}
	for (ques k:tmp) {
		int op=k.op,x=k.x,v=k.v,t=k.tim;
		if (op==1) {
			for (int k:pt) if (vis[x][mp[k]]) a[k]=v,mx[k]=t;
			tg1[x]={v,t};
		}
		else if (op==2) {
			for (int k:pt) if (vis[x][mp[k]]) a[k]=min(a[k],v);
			tg2[x]=min(tg2[x],v);
		}
		else {
			f(i,id[mx[x]]+1,idx-1) a[x]=min(a[x],mn[i][x]);
			for (ques k:qs[id[mx[x]]]) if (k.op==2&&k.tim>mx[x]&&vis[k.x][mp[x]]) a[x]=min(a[x],k.v);
			printf("%d\n",a[x]);
		}
	}
	f(u,1,n) {
		mn[idx][u]=tg2[u];
		if (tg1[u].t) a[u]=tg1[u].v,mx[u]=tg1[u].t;
		for (int v:e[u]) {
			if (tg1[u].t>tg1[v].t) tg1[v]=tg1[u];
			tg2[v]=min(tg2[v],tg2[u]);
		}
	}
	qs[idx]=tmp;
}

inline void solve() {
	read(n);read(m);read(q);
	f(i,1,n) read(a[i]);
	f(i,1,m) {
		int u,v;read(u);read(v);
		e[u].push_back(v);
	}
	int len=0,cnt=0;
	vector<ques> st;
	f(i,1,q) {
		int op,x,v=0;
		read(op);read(x);
		if (op!=3) read(v);
		st.push_back({op,x,v,i});
		if (++len==B) {
			work(st,++cnt);
			len=0;
			st.clear();
		}
	}
	if (len) work(st,++cnt);
	clear(cnt);
}

int main() {
	freopen("recall.in","r",stdin);
	freopen("recall.out","w",stdout);
	int C,T;read(C);read(T);
	while (T--) solve();
	return 0;
}

Tenzing and Tree

题意

给定一棵 \(n\) 个节点的树,你需要给 \(k\) 个节点染成黑色,定义一种染色方案的权值为对于每条边,断掉这条边后两个连通块的黑点个数差的绝对值之和。

对于 \(k=0,1,\dots ,n\),输出权值最大值。

\(1\le n\le 5000\)

solution

神秘贪心。

考虑去掉绝对值,黑点权值为 \(1\),否则为 \(0\),确定带权重心 \(r\) 为根,令 \(i\) 的子树内有 \(sz_i\) 个黑色节点,此时由于重心的性质,可以得到答案为 \(\sum\limits_{i\ne r} k-2\times sz_i=(n-1)k-2\times \sum\limits_{w_i=1}dep_i\)

枚举 \(r\),我们希望染成黑点的深度之和最小,排序解决即可。

但是这样染色一定能使 \(r\) 是重心吗?答案是不一定,但是若 \(r\) 不为重心,这个方案一定会更劣,不会影响答案,原因是将重心向黑色节点个数最多的子树方向移动得到的答案更优,所以不会影响最终答案。

时间复杂度 \(O(n^2)\)

Code

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=5e3+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int t[N],ans[N];
vector<int> e[N];

inline void dfs(int now,int fa,int dep) {
	t[dep]++;
	for (int v:e[now]) if (v^fa) dfs(v,now,dep+1);
} 

int main() {
	read(n);
	f(i,2,n) {
		int u,v;read(u);read(v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	f(i,1,n) {
		dfs(i,0,0);
		int now=0,sum=0;
		f(j,1,n) {
			while (!t[now]) now++;
			sum+=now;t[now]--;
			ans[j]=max(ans[j],(n-1)*j-2*sum);
		}
	}
	f(i,0,n) printf("%d ",ans[i]);
	return 0;
}

posted @ 2026-07-14 20:08  Ff472130  阅读(4)  评论(0)    收藏  举报