《信息学奥赛一本通·高手专项训练》集训 Day 10

并查集

100+0+0=100/Rank 9\color{Green}100\color{Black}+\color{Red}0\color{Black}+\color{Red}0\color{Black}=\color{Orange}100\color{Black}/\text{Rank 9}

A. 最低热量\color{#FFC116}\text{A. 最低热量}

题目

小明将学校中的所有地点编号为 11nn,其中起点被编号为 SS,终点被编号为 TT

学校中有 mm 条连接两个点的双向道路,保证从任意一个点可以通过道路到达学校中的所有点。每条路都有一个温度 tt,及通过一条路所需的时间 cc,在温度为 tt 的路径跑单位时间,就会使她的热量增加 tt

在经过的所有道路中最高温度最低的前提下,使小明到达终点时的热量最低(从起点出发时,小明的热量为 00),输出此时小明经过路径的最高温度和小明到达终点时的热量。

题解

我们先考虑前提——最高温度最低,但必须满足 SSTT 是联通的,于是我们可以枚举最高温度,把新符合条件的边加进来,用并查集维护其连通性,知道找到最小的最高温度。

然后直接跑最短路求答案即可。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 5e5 + 10, M = 1e6 + 10;
int n, m, S, T, l, r;
int head[N], ver[M << 1], nxt[M << 1], te[M << 1], tot = 1;
ll ce[M << 1], d[N], v[N], ans;
int fa[N];
vector<int> e[N];
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void add(int a, int b, int t, ll c) {
    ver[++tot] = b;
    te[tot] = t;
    ce[tot] = c;
    nxt[tot] = head[a];
    head[a] = tot;
}
void Dijkstra(int mid) {
    memset(d, 0x3f, sizeof(d));
    memset(v, 0, sizeof(v));
    priority_queue<pair<ll, int> > q;
    q.push(make_pair(0, S));
    d[S] = 0;
    while (q.size()) {
        int x = q.top().second;
        q.pop();
        if (v[x])
            continue;
        v[x] = 1;
        for (int i = head[x]; i; i = nxt[i]) {
            if (te[i] > mid)
                continue;
            int y = ver[i];
            ll cost = (ll)te[i] * ce[i];
            if (d[y] > d[x] + cost) {
                d[y] = d[x] + cost;
                q.push(make_pair(-d[y], y));
            }
        }
    }
    ans = d[T];
}
int main() {
    freopen("running.in", "r", stdin);
    freopen("running.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= n; i++) fa[i] = i;
    for (int i = 1; i <= m; i++) {
        int a, b, t;
        ll c;
        a = read();
        b = read();
        t = read();
        c = read();
        add(a, b, t, c);
        add(b, a, t, c);
        r = max(r, t);
        e[t].push_back(tot);
    }
    S = read();
    T = read();
    for (l = 0; l <= r; l++) {
        for (int j = 0; j < e[l].size(); j++) {
            fa[get(ver[e[l][j]])] = get(ver[e[l][j] ^ 1]);
        }
        if (get(S) == get(T))
            break;
    }
    Dijkstra(l);
    write(l);
    putchar(' ');
    write(ans);
    return 0;
}

B. 一道树论\color{#52C41A}\text{B. 一道树论}

题目

小 W 现在有一个点集 SS 和一颗树,树上有 nn 个点和 n1n-1 条有边权的无向边。

但是小 W 很好奇,如果他想要断掉一些边,使得点集 SS 内的点两两不连通,怎么做才能使要断掉的边的边权和最小呢?

他把这个问题丢给了你,想让你来回答,你只用告诉他要断的边的边权和是多少即可。

但他又觉得太简单了,所以他会一个个把 SS 这些点以某个顺序插入 SS 中(初始 SS 为空),你要在每次插入后都回答他这个问题。

题解

删除边不好维护,我们可以把顺序倒过来改成加边,用并查集维护点的连通性。

为了使删除的边的边权和最小,反过来就要使加入的边的边权和最大,因此将边从大到小排序,逐条考虑。

对于一条要加入的边 uvu\leftrightarrow v,我们考虑它最晚得在原序中什么时候加入,因为加入后集合 Vu,VvV_u,V_v 就连通了,所以他最晚加入的时间就是两个集合中最早被加入到集合 SS 的点被加入的时间的最大值。

最后通过边加入的时间计算答案即可。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 5e5 + 10;
int n, a[N], fa[N], ti[N], d[N], mx[N];
int head[N], ver[N << 1], nxt[N << 1], v[N << 1], tim[N << 1], tot = 1;
ll edge[N << 1], ans[N];
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void add(int x, int y, ll z) {
    ver[++tot] = y;
    edge[tot] = z;
    nxt[tot] = head[x];
    head[x] = tot;
}
bool cmp(int x, int y) { return edge[2 * x] > edge[2 * y]; }
int main() {
    freopen("tree.in", "r", stdin);
    freopen("tree.out", "w", stdout);
    n = read();
    for (int i = 1; i < n; i++) {
        int x, y;
        ll z;
        x = read();
        y = read();
        z = read();
        add(x, y, z);
        add(y, x, z);
    }
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        ti[a[i]] = i;
        fa[i] = i;
        d[i] = i;
    }
    sort(d + 1, d + n, cmp);
    for (int i = 1; i < n; i++) {
        int u = get(ver[d[i] * 2]), v = get(ver[d[i] * 2 + 1]);
        tim[d[i]] = max(ti[u], ti[v]);
        ti[u] = min(ti[u], ti[v]);
        fa[v] = u;
        ans[tim[d[i]]] += edge[d[i] * 2];
    }
    for (int i = 1; i <= n; i++) {
        ans[i] += ans[i - 1];
        write(ans[i]);
        putchar('\n');
    }
    return 0;
}

C. 树上除法\color{#52C41A}\text{C. 树上除法}

题目

给出一棵包含 nn 个点的树,每条边有一个权值。你需要维护这棵树,共 QQ 次操作,操作有两种:

1 x y p\texttt{1 x y p}:一个带有数字 pp 的点,将要从点 xx 走到点 yy,每次走过一条值为 qq 的边,pp 就会变成 pq\left\lfloor\frac{p}{q}\right\rfloor,问最终 pp 的值。

2 x y\texttt{2 x y}:将第 xx 条边的边权修改为 yy,保证修改后的权值小于等于原来的权值且不会小于 11

题解

发现若边的权值都大于 11,那么 pp 最多走 6262 条边就会变成 00,所以可以暴力在树上走,不会超时。

但是权值为 11 的边走起来是没用的,而且以后也不会变大,所以我们可以用并查集把这条边的两个端点合并,加快走边的速度。

走边时,我们可以求出 xxyy 的最近公共祖先 zz,先从 xx 走到 zz,再从 yy 走到 zz,这与原本的顺序不同,但我们仍可以证明他和原序的结果相同,只要证明 abc=abc\left\lfloor\frac{a}{bc}\right\rfloor=\left\lfloor\frac{\left\lfloor\frac{a}{b}\right\rfloor}{c}\right\rfloor 即可。

ab=ab+r(0r<1)\frac{a}{b}=\left\lfloor\frac{a}{b}\right\rfloor+r(0\le r<1),那么 abc=abc+rc=abc\left\lfloor\frac{a}{bc}\right\rfloor=\left\lfloor\frac{\left\lfloor\frac{a}{b}\right\rfloor}{c}+\frac{r}{c}\right\rfloor=\left\lfloor\frac{\left\lfloor\frac{a}{b}\right\rfloor}{c}\right\rfloor

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1e5 + 10;
int n, q, Log[N], dep[N], fa[N][19], f[N];
int head[N], ver[N << 1], nxt[N << 1], tot = 1;
ll edge[N << 1], w[N];
void add(int x, int y, ll z) {
    ver[++tot] = y;
    edge[tot] = z;
    nxt[tot] = head[x];
    head[x] = tot;
}
void dfs(int x) {
    for (int i = head[x]; i; i = nxt[i]) {
        int y = ver[i];
        if (y == fa[x][0])
            continue;
        fa[y][0] = x;
        dep[y] = dep[x] + 1;
        w[y] = edge[i];
        dfs(y);
    }
}
int lca(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);
    while (dep[x] > dep[y]) {
        x = fa[x][Log[dep[x] - dep[y]]];
    }
    if (x == y)
        return x;
    for (int i = Log[dep[x] + 1]; i >= 0; i--) {
        if (fa[x][i] != fa[y][i]) {
            x = fa[x][i];
            y = fa[y][i];
        }
    }
    return fa[x][0];
}
int get(int x) { return f[x] == x ? x : f[x] = get(f[x]); }
int main() {
    freopen("division.in", "r", stdin);
    freopen("division.out", "w", stdout);
    n = read();
    q = read();
    for (int i = 1; i < n; i++) {
        int x, y;
        ll z;
        x = read();
        y = read();
        z = read();
        add(x, y, z);
        add(y, x, z);
    }
    Log[0] = -1;
    for (int i = 1; i <= n; i++) {
        Log[i] = Log[i >> 1] + 1;
        f[i] = i;
    }
    dfs(1);
    for (int j = 1; j <= Log[n] + 1; j++)
        for (int i = 1; i <= n; i++) fa[i][j] = fa[fa[i][j - 1]][j - 1];
    for (int i = 1; i < n; i++) {
        if (edge[i * 2] == 1) {
            int x = ver[i * 2], y = ver[i * 2 + 1];
            if (dep[x] > dep[y])
                swap(x, y);
            x = get(x);
            y = get(y);
            if (x ^ y)
                f[y] = x;
        }
    }
    while (q--) {
        ll ty, x, y, p;
        ty = read();
        x = read();
        y = read();
        if (ty == 1) {
            p = read();
            ll z = get(lca(x, y));
            x = get(x);
            y = get(y);
            while (x != z && p) {
                p /= w[x];
                x = get(fa[x][0]);
            }
            if (!p) {
                puts("0");
                continue;
            }
            while (y != z && p) {
                p /= w[y];
                y = get(fa[y][0]);
            }
            if (!p) {
                puts("0");
                continue;
            }
            write(p);
            putchar('\n');
        } else {
            int u = ver[x * 2], v = ver[x * 2 + 1];
            if (dep[u] > dep[v])
                swap(u, v);
            w[v] = y;
            if (y == 1) {
                u = get(u);
                v = get(v);
                if (u ^ v)
                    f[v] = u;
            }
        }
    }
    return 0;
}

最小生成树

100+100+20=220/Rank 5\color{Green}100\color{Black}+\color{Green}100\color{Black}+\color{Red}20\color{Black}=\color{#92E411}220\color{Black}/\text{Rank 5}

A. 路径统计\color{#52C41A}\text{A. 路径统计}

题目

给出一个包含 nn 个点 mm 条边的无向连通图,有如下定义:

  • 定义一条路径的价值为路径上最小边的权值。
  • 定义 dist(i,j)\text{dist}(i,j) 为起点为 ii,终点为 jj 的所有路径中,价值最大的路径的价值。

现在,给出一个 kk,请你求出第 kk 大的 dist(i,j)\text{dist}(i,j)

题解

为了使路径价值最大,我们求该图的最大生成树,建出其 Kruskal\text{Kruskal} 重构树,重构树里任意两个子节点的 lca\text{lca} 都是这两点间路径的最小边的权值,即 dist(i,j)\text{dist}(i,j),按顺序累计对数知道第 kk 大即可。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 2e5 + 10, M = 4e5 + 10;
int n, m, fa[N], tot, dist[N];
int h1[N], v1[M << 1], n1[M << 1], e1[M << 1], t1 = 1;
ll k, sl[N], sr[N], sze[N];
void add1(int x, int y, int z) {
    v1[++t1] = y;
    e1[t1] = z;
    n1[t1] = h1[x];
    h1[x] = t1;
}
void init() {
    for (int i = 1; i <= 2 * n; i++) fa[i] = i;
    for (int i = 1; i <= n; i++) sze[i] = 1;
}
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
struct Kruskal_rebuild {
    pair<int, pair<int, int> > a[M];
    int m;
    void ask(int n) {
        m = 0;
        tot = n;
        init();
        for (int i = 1; i <= n; i++) {
            for (int j = h1[i]; j; j = n1[j]) {
                int y = v1[j], z = e1[j];
                if (i < y)
                    a[++m] = (make_pair(z, make_pair(i, y)));
            }
        }
        sort(a + 1, a + m + 1);
        for (int i = 1; i <= m; i++) {
            int u = a[i].second.first;
            int v = a[i].second.second;
            if (get(u) != get(v)) {
                int w = ++tot;
                sze[w] = sze[get(u)] + sze[get(v)];
                sl[w] = sze[get(u)];
                sr[w] = sze[get(v)];
                fa[get(u)] = w;
                fa[get(v)] = w;
                dist[w] = -a[i].first;
            }
        }
    }
} tu;
int main() {
    freopen("path.in", "r", stdin);
    freopen("path.out", "w", stdout);
    n = read();
    m = read();
    k = ((ll)n) * ((ll)n - 1) / 2 - read();
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        x = read();
        y = read();
        z = -read();
        add1(x, y, z);
        add1(y, x, z);
    }
    tu.ask(n);
    for (int i = tot; i > n; i--) {
        k -= (ll)sl[i] * (ll)sr[i];
        if (k < 0) {
            cout << dist[i];
            return 0;
        }
    }
    return 0;
}

B. 黑白之树\color{#3498DB}\text{B. 黑白之树}

题目

给出一个包含 nn 个点 mm 条边的无向带权连通图,点的编号为 0n10\sim n-1,每条边是黑色或白色。

请你求一棵最小权的恰好有 kk 条白色边的生成树。题目保证有解。

题解

P2619 [国家集训队]Tree I

g(x)\text{g}(x) 表示恰好选 xx 条白边得到的最小生成树的权值,显然这是一个下凸包,因为一定存在最优的一个 x0x_0,对于任意 x1<x0<x2x_1<x_0<x_2,有 g(x1)g(x0),g(x2)g(x0)\text{g}(x_1)\le \text{g}(x_0),\text{g}(x_2)\le \text{g}(x_0)。这满足 wqs\text{wqs} 二分的条件,于是我们可以二分白点权值的变化值,即斜率,用 Kruskal\text{Kruskal} 求出其去这个下凸包的切点,可得到相应的 kk',再选择二分的边界变化即可。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 5e4 + 10, M = 1e5 + 10;
int n, m, k, fa[N], ans;
int head[N], ver[M << 1], nxt[M << 1], col[M << 1], edge[M << 1], tot;
void add(int x, int y, int c, int z) {
    ver[++tot] = y;
    col[tot] = c;
    edge[tot] = z;
    nxt[tot] = head[x];
    head[x] = tot;
}
void init() {
    for (int i = 1; i <= n; i++) fa[i] = i;
}
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void merge(int x, int y) { fa[get(x)] = get(y); }
struct Kruskal {
    pair<pair<int, int>, pair<int, int> > a[M];
    int m, ans, cnt;
    void ask(int n, int mid) {
        m = ans = cnt = 0;
        init();
        for (int i = 1; i <= n; i++) {
            for (int j = head[i]; j; j = nxt[j]) {
                int y = ver[j], c = col[j], z = edge[j];
                if (i < y)
                    a[++m] = (make_pair(make_pair(z + c * mid, c), make_pair(i, y)));
            }
        }
        sort(a + 1, a + m + 1);
        for (int i = 1; i <= m; i++) {
            int u = a[i].second.first;
            int v = a[i].second.second;
            if (get(u) != get(v)) {
                merge(u, v);
                cnt += a[i].first.second;
                ans += a[i].first.first;
            }
        }
    }
} tu;
int main() {
    freopen("tree.in", "r", stdin);
    freopen("tree.out", "w", stdout);
    n = read();
    m = read();
    k = read();
    for (int i = 1; i <= m; i++) {
        int x, y, c, z;
        x = read() + 1;
        y = read() + 1;
        z = read();
        c = 1 - read();
        add(x, y, c, z);
        add(y, x, c, z);
    }
    int l = -101, r = 101;
    while (l < r) {
        int mid = (l + r) >> 1;
        tu.ask(n, mid);
        if (tu.cnt <= k) {
            ans = tu.ans - k * mid;
            r = mid;
        } else
            l = mid + 1;
    }
    cout << ans << endl;
    return 0;
}

C. 生成树约数\color{#3498DB}\text{C. 生成树约数}

题目

给出一个包含 nn 个点 mm 条边的无向连通图,定义一个生成树的权值为该生成树上所有边权的最大公约数,请你求出该图中所有生成树权值的最小公倍数。

题解

如果我们把所有边权都唯一分解,那么可以发现每个质数对答案的贡献是互不干预的,于是我们可以对每个质数分开计算,这样,公约数和公倍数的计算就变成了对质数因子个数的取 min\minmax\max

那么对于质数 pp 来说,原图的所有生成树的权值的最小公倍数就为原图的最大瓶颈生成树的权值最小的点的权值,注意这里的权值已改为 pp 因子的个数。

而最大生成树一定是最大瓶颈生成树,所以对于每个质数求相应的最大生成树再计算贡献即可。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1e3 + 10, M = 1e5 + 10;
int n, m, fa[N];
vector<pair<ll, pair<int, int> > > e[M];
ll np, prime[M], fac[M], ans = 1;
int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
void sieve() {
    for (int i = 2; i < (1 << 15); i++) {
        if (!fac[i]) {
            fac[i] = i;
            prime[++np] = i;
        }
        for (int j = 1; j <= np; j++) {
            if (prime[j] > fac[i] || prime[j] * i + 1 > (1 << 15))
                break;
            fac[prime[j] * i] = prime[j];
        }
    }
}
ll qmi(ll a, ll b) {
    ll ans = 1;
    while (b) {
        if (b & 1)
            ans *= a;
        a *= a;
        b >>= 1;
    }
    return ans;
}
struct Kruskal {
    int m;
    void ask(int n, ll y) {
        m = 0;
        for (int i = 1; i <= n; i++) {
            fa[i] = i;
        }
        sort(e[prime[y]].begin(), e[prime[y]].end());
        for (int i = 0; i < e[prime[y]].size(); i++) {
            int u = e[prime[y]][i].second.first;
            int v = e[prime[y]][i].second.second;
            if (get(u) != get(v)) {
                fa[get(u)] = get(v);
                m++;
                if (m == n - 1) {
                    ans *= qmi(prime[y], -e[prime[y]][i].first);
                    break;
                }
            }
        }
    }
} tu;
int main() {
    freopen("divisor.in", "r", stdin);
    freopen("divisor.out", "w", stdout);
    n = read();
    m = read();
    sieve();
    for (int i = 1; i <= m; i++) {
        int x, y;
        ll z;
        x = read();
        y = read();
        z = read();
        for (ll j = 1; prime[j] * prime[j] <= z; j++)
            if (z % prime[j] == 0) {
                ll cnt = 0;
                while (z % prime[j] == 0) {
                    z /= prime[j];
                    cnt++;
                }
                e[prime[j]].push_back(make_pair(-cnt, make_pair(x, y)));
            }
        if (z > 1)
            e[z].push_back(make_pair(-1, make_pair(x, y)));
    }
    for (int i = 1; i <= np; i++) tu.ask(n, i);
    write(ans);
    return 0;
}
posted @ 2022-08-10 19:41  luckydrawbox  阅读(60)  评论(0)    收藏  举报  来源