并查集 & 堆
堆
一棵树(二叉树),每个子节点都大于(或小于)他的父节点。
父亲权值不小于儿子权值(大根堆),父亲权值不大于儿子权值(小根堆),默认是大根堆
手写堆:
- 插入:先插入进去,然后一个个往上看,看看是不是比父亲大(或小),如果不满足堆的对应性质,就删掉。
- 删除:把根节点和最后一个节点交换,然后从根节点往下看看是否满足要求,如果不满足就交换,直到满足这个堆的对应性质为止。
对顶堆:由一个大根堆和一个小根堆组成
并查集
优化:启发式合并 && 路径压缩。都很常用,就不多说了。(路径压缩的话就不是很好做可持久化了,但是可以用 \(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 专业网络
这题和 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;
}