并查集 & 堆

 一棵树(二叉树),每个子节点都大于(或小于)他的父节点。

 父亲权值不小于儿子权值(大根堆),父亲权值不大于儿子权值(小根堆),默认是大根堆

 手写堆:

  1. 插入:先插入进去,然后一个个往上看,看看是不是比父亲大(或小),如果不满足堆的对应性质,就删掉。
  2. 删除:把根节点和最后一个节点交换,然后从根节点往下看看是否满足要求,如果不满足就交换,直到满足这个堆的对应性质为止。

 对顶堆:由一个大根堆和一个小根堆组成

并查集

 优化:启发式合并 && 路径压缩。都很常用,就不多说了。(路径压缩的话就不是很好做可持久化了,但是可以用 \(rope\)rope大法好!

(以下全部来自 OI Wiki)

T1

\(n\) 个点,初始时均为孤立点。接下来有 \(m\) 次加边操作,第 \(i\) 次操作在 \(a_i\)\(b_i\) 之间加一条无向边。设 \(L(i,j)\) 表示 \(i\)\(j\) 最早在 \(L(i,j)\) 次操作后连通。

\(m\) 次操作后,求出 \(\sum\limits_{i = 1}^{n} \sum\limits_{j = i + 1} ^{n} L(i,j)\)

solution

 基础并查集的应用,并查集记录一下子树的大小。考虑每次操作的贡献,如果第 i 次操作的 a[i] 和 b[i] 不属于同一个子树,那么这次的操作的贡献就是 siz[find(a[i])] * siz[find(b[i])] * i,每次操作的时候记录操作提供的贡献最后再加起来就是答案了。

T2

\(n\) 个点,初始时均为孤立点。接下来有 \(m\) 次加边操作,第 \(i\) 次在 \(a_i\)\(b_i\) 之间加一条无向边。

接下来有 \(q\) 次询问,第 \(i\) 次询问 \(u_i\)\(v_i\) 最早在第几次操作后连通

solution

 考虑在并查集合并的时候记录并查集生成树,也就是如果第 i 次操作的两个点不属于同一个子树,那么就把这条边纳入生成树中,边权是 i ,那么每一次询问其实就是查询 u 到 v 路径上的边权最大值,可以用树上倍增或者树链剖分的方法维护。

 还可以建一个类似于 kruskal 重构树一样的结构,每次查询就查询 LCA 的值就可以了。

T3

\(n\) 个点,初始时均为孤立点,接下来有 \(m\) 次加边,每一次在两个节点中加一条无向边。

接下来有 \(q\) 次询问,每一次询问都询问一个点在某一次操作之后所在连通块的大小。

solution

 离线算法:将询问按照 t[i] 的大小从小到大排序,在加边的过车好难过中是用并查集顺便处理询问即可

 在线算法:只能使用 Kruskal 重构树。每一次加边的时候,就用加一个点令他的权值为 i ,然后查询的时候就是 x[i] 在重构树种最大的一个连通块使得连通块中的点权最大值不超过 t[i] ,那么这个连通块的叶子结点数就是所求答案

 由于操作的编号是递增的,所以重构树上的父亲的权值一定是大于子节点的权值的,所以我们可以直接倍增找到重构树上从 x[i] 到根节点的路径上找到点权最大的不超过 t[i] 的节点。

T4

给一个长度为 \(n\) 的序列,一开始全部为 \(0\),接下来进行 \(m\) 次操作:1. 令 \(a_x = 1\)。2. 求\(a_x,a_{x + 1}a_{x + 2}...a_n\) 中从左往右数第一个为 \(0\) 的位置。

solution

 建立一个并查集,\(h[i]\) 表示 \(a[i],a[i+1]...a[n]\) 中第一个为 \(0\) 的位置,对于每一次一操作,我们就令 \(h[x] = h[x + 1]\) 就可以了。

带权并查集

 在并查集的边上定义某种权值(一半是定义点到祖宗的距离)、以及这种权值在路径压缩时产生的运算,从而解决更多的问题。

 经典例题就是 P2024 食物链

6346 专业网络

link

 这题和 Voting (Hard Version) 是一样的,但是一个紫题一个绿题,气。然后这个紫题要开 \(lwl\)

 一开始假设一个很极端的情况,所有朋友都是用钱买的,那么总价钱就很容易求,然后我们再考虑最多能够省下多少钱。显而易见的是,想要省钱就只能靠跟风()。

 于是从后往前枚举跟风过来的人,然后计算过后就把他扔到队列里面(队列里面就是被收买的人)如果我们把剩下的人都收买了都没有办法让他跟风,就把他也收买了。

 在所有可以省下的钱里面取一个最大值,再用总的减去他,就是所要花的钱的最小值了。

点击查看代码
int n;
pii w[N];

int main(){
	n = fr();
	int sum = 0;
	for (int i = 1; i <= n; i ++) {
		w[i].fi = fr(); // 要结交的人
		w[i].se = fr(); // 要花的钱
		sum += w[i].se;
	}
	sort(w + 1,w + 1 + n);
	priority_queue<int,vector<int>,greater<int> > q;
	int ans = 0,p = 0;
	// 省的最多的,当前省的
	for (int i = n; i; i --) {
		// 倒序枚举
		p += w[i].se;
		q.push(w[i].se);
		while (w[i].fi > n - q.size()) {
			// 不够的话
			auto t = q.top();
			q.pop();
			p -= t; // 跟别人花钱交朋友
		}
		ans = max(ans,p);
	}
	fw(sum - ans);
	return 0;
}

练习

 评价是 \(A\) 题很水,\(B\) 题数据难得比洛谷强,\(C\) 题根本来不及看。

A.九转大肠

 感觉这一题就是贪心加上堆。

 很显然的一点是我们想让每一个人都尽量尽早的干活,而我们需要 \(l\) 个配对,所以我们先将清洗大肠的和烧制大肠的人分开考虑,将每一种人都算出前 \(k\) 个较小的时间。

 然后我们就匹配一下,尽量让这两个时间的和合起来最小,所以我们就用最小的 \(a_i\) 去匹配处理出来的最大的 \(b_i\) ,再以此类推,把这种匹配后的和取一个最小值就是我们求的值了。

点击查看代码
int l,n,m;
lwl w1[N],w2[N];

int main(){
	l = fr(),n = fr(),m = fr();
	priority_queue<pii,vector<pii>,greater<pii> > a,b;
	// 下一次可以开始的时间,所需的时间
	for (int i = 1; i <= n; i ++) {
		int t = fr();
		a.push({t,t});
	}
	for (int j = 1; j <= m; j ++) {
		int t = fr();
		b.push({t,t});
	}
	lwl ans = 0;
	for (int i = 1; i <= l; i ++) {
		auto t = a.top();
		a.pop();
		w1[i] = t.fi;
		a.push({t.fi + t.se,t.se});
		t = b.top();
		b.pop();
		b.push({t.fi + t.se,t.se});
		w2[i] = t.fi;
	}
	for (int i = 1; i <= l; i ++) {
		ans = max(ans,w1[i] + w2[l - i + 1]);
	}
	fw(ans);
	return 0;
}

B.魔法商店

 这一题在洛谷上面过了,但是在信友队上面只有 \(60\) 分。信友队的数据竟然比洛谷强了,我哭死。

 然后这个是我六十分的错误代码,讲究的就是一个完全不知道正确性的贪心:

点击查看代码
int n,k;
lwl m;
pii w[N];
bool flag[N];

bool cmp(pii a,pii b) {
	if (a.fi.se == b.fi.se) return a.fi.fi > b.fi.fi;
	return a.fi.se < b.fi.se;
}

int h(int i) {
	return w[i].fi.fi - w[i].fi.se;
}

int main(){
	n = fr(),m = fr(),k = fr();
	for (int i = 1; i <= n; i ++) {
		w[i].fi.fi = fr();
		w[i].fi.se = fr();
		w[i].se = i;
	}
	sort(w + 1,w + 1 + n,cmp);
	int ans = 0;
	priority_queue<int,vector<int>,greater<int> > q;
	// 哪些要用优惠卷(存差值)
	for (int i = 1; i <= k; i ++) {
		if (m >= w[i].fi.se) {
			ans ++;
			m -= w[i].fi.se;
		}
		q.push(w[i].fi.fi - w[i].fi.se);
		flag[w[i].se] = true;
	}
	sort(w + 1,w + 1 + n);
	for (int i = 1; i <= n; i ++) {
		if (flag[w[i].se]) continue;
		if (m >= w[i].fi.fi && ((!q.size()) || h(i) < q.top())) {
			// 用了优惠卷没有其他的优惠力度大
			ans ++;
			m -= w[i].fi.fi;
			flag[w[i].se] = true;
			continue;
		}
		if (q.size() && m >= w[i].fi.se + q.top() && h(i) >= q.top()) {
			ans ++;
			m -= w[i].fi.se + q.top();
			q.pop();
			q.push(w[i].fi.fi - w[i].fi.se);
			flag[w[i].se] = true;
		}
	}
	for (int i = 1; i <= n; i ++) {
		if (flag[w[i].se]) continue;
		if (m >= w[i].fi.fi) {
			m -= w[i].fi.fi;
			ans ++;
		}
	}
	fw(ans);
	return 0;
}

 然后正解是弄三个优先队列,把每一个都存一下,然后一开始也是把减价后前 \(k\) 小的物品全部都选上(可以证明这 \(k\) 个物品是肯定要选的),然后我们通过三个优先队列分别维护 \(p,c,p - c\) 的最小值(这里的 \(p - c\) 指的是到现在为止用了优惠的物品优惠的价格)

 然后我们每次都在直接购买(\(p\) 的优先队列)和将原来的用了优惠的物品用原价,再用优惠购买一个新的物品(\(c,p-c\))的队列。

 好像是一个反悔贪心。(然后这一题记得要开 \(lwl\) !)

点击查看代码
int n,k;
lwl m;
pii w[N];
bool flag[N];

int h(int i) {
	return w[i].fi - w[i].se;
}

bool cmp(pii a,pii b) {
	return a.se < b.se;
}

int main(){
	n = fr(),m = fr(),k = fr();
	for (int i = 1; i <= n; i ++) {
		w[i].fi = fr();
		w[i].se = fr();
	}
	sort(w + 1,w + 1 + n,cmp);
	int ans = 0;
	priority_queue<int,vector<int>,greater<int> > dq;
	priority_queue<pii,vector<pii>,greater<pii> > cq,pq;
	// 哪些要用优惠卷(存差值)
	for (int i = 1; i <= k; i ++) {
		if (m >= w[i].se) {
			ans ++;
			m -= w[i].se;
		} else {
			fw(ans);
			return 0;
		}
		dq.push(h(i));
	}
	for (int i = k + 1; i <= n; i ++) {
		cq.push({w[i].se,i});
		pq.push({w[i].fi,i});
	}
	for (int i = k + 1; i <= n; i ++) {
		while (pq.size() && flag[pq.top().se]) pq.pop();
		while (cq.size() && flag[cq.top().se]) cq.pop();
		auto id1 = cq.top().se,id2 = pq.top().se;
		auto w1 = cq.top().fi + dq.top();
		auto w2 = pq.top().fi;
		if (w1 < w2) {
			if (m >= w1) {
				m -= w1;
				cq.pop(),dq.pop();
				dq.push(h(id1));
				flag[id1] = true;
				ans ++;
			}
		} else {
			if (m >= w2) {
				ans ++;
				m -= w2;
				pq.pop();
				flag[id2] = true;
			}
		}
	}
	fw(ans);
	return 0;
}

C.金酒之杯

 这一题理论上来说应该是好写的,但是考试的时候去和第二题斗智斗勇去了。算了,不管他。

 就是把每一个国家的点都压到一个点去(包括中间经过的路径),然后建出一个新图,新图中叶子(如果根节点的度数为 \(1\) 也算作叶子节点)节点的数量除以二向上取整就是答案。

 这个过程可以通过样例二推断得。

点击查看代码
int n,k;
int h[N],w[N];
int d[N];
int gs[N];
vector<int> e[N];
int de[N],fa[N][25];

void dfs(int u,int father) {
	de[u] = de[father] + 1;
	fa[u][0] = father;
	for (int k = 1; k <= log2(de[u]); k ++) 
		fa[u][k] = fa[fa[u][k - 1]][k - 1];
	for (auto v : e[u]) {
		if (v == father) continue;
		dfs(v,u);
	}
}

int LCA(int x,int y) {
	if (de[x] < de[y]) swap(x,y);
	int dh = de[x] - de[y];
	int kmax = log2(dh);
	for (int k = kmax; k >= 0; k --) {
		if ((dh >> k) & 1) {
			x = fa[x][k];
		}
	}
	
	if (x == y) return x;
	
	kmax = log2(de[x]);
	for (int k = kmax; k >= 0; k --) {
		if (fa[x][k] != fa[y][k]) {
			x = fa[x][k];
			y = fa[y][k];
		}
	}
	
	return fa[x][0];
}

int find(int x) {
	if (x == 1) {
		return 1;
	}
	if (x != h[x]) h[x] = find(h[x]);
	return h[x];
}

void merge(int x,int top) {
	if (x == top) return ;
	while (find(x) != h[top] && de[x] > de[top]) {
		x = h[x];
		h[x] = h[top];
		x = fa[x][0];
	}
}

int main(){
	n = fr(),k = fr();
	for (int i = 1; i <= n; i ++) {
		h[i] = i;
	}
	for (int i = 1; i < n; i ++) {
		int a = fr(),b = fr();
		e[a].push_back(b);
		e[b].push_back(a);
	}
	dfs(1,0);
	for (int i = 1; i <= n; i ++) {
		gs[i] = fr();
		if (!w[gs[i]]) {
			w[gs[i]] = i;
			continue;
		}
		int t = LCA(i,w[gs[i]]);
		merge(i,t);
		merge(w[gs[i]],t);
	}
	for (int i = 1; i <= n; i ++) {
		int u = find(i);
		for (auto j : e[i]) {
			int v = find(j);
			if (u == v) continue;
			d[u] ++,d[v] ++;
		}
	}
	int cnt = 0;
	for (int i = 1; i <= n; i ++) {
		int hi = find(i);
		if (hi != i) continue;
		if (d[i] == 2) cnt ++;
	}
	fw((cnt + 1) / 2);
	return 0;
}
posted @ 2023-07-26 20:29  jingyu0929  阅读(27)  评论(0)    收藏  举报