【二分好題】

名人名言

Um_nik: Stop learning useless algorithms, go and solve some problems, learn how to use binary search!

題目

背包 HLP3461

題目描述

\(n\) 個物品,每個物品有對應的價格和價值

\(m\) 次詢問,每次詢問會有 \(c\) 的本金,之後無腦選擇能買得起的最貴物品買走(若有多個最貴的就選價值最高的),直到不能買為止,問買走的物品價值和。(\(n \leq 10 ^ 5\)

解題思路

首先對物品先按價格, 後按價值從大到小排序。

注意到對於每次購買一段東西,本金減少至少一半,所以消費的次數為 \(O(log\ n)\) 級別的

這時候我們知道這次購買的右端點 \(r\) (即上次購買的最後一個的下一個), 則可以利用前綴和求出左端點 \(l\)

程式碼

#include <bits/stdc++.h>
#define int long long
const int N = 1e5 + 5, inf = 1e18;
int n, m, x, ans, last;
int sum[N], v[N];
struct product {
	int a, b;
	bool operator < (const product &x) const {return a == x.a ? b > x.b : a > x.a;}
} a[N];
signed main() {
	scanf("%lld%lld", &n, &m);
	for (int i = 1; i <= n; ++i) scanf("%lld%lld", &a[i].a, &a[i].b);
	std::sort(a + 1, a + n + 1);
	for (int i = 1; i <= n; ++i) sum[i] = a[i].a + sum[i - 1];
	for (int i=  1; i <= n; ++i) v[i] = a[i].b + v[i - 1];
	while (m--) {
		scanf("%lld", &x);
		last = ans = 0;
		while (x) {
			int l = std::lower_bound(a + last + 1, a + n + 1, (product){x, inf}) - a;
			if (l > n) break;
			int r = std::upper_bound(sum + l + 1, sum + n + 1, x - a[l - 1].a) - sum - 1;
			x -= sum[r] - sum[l - 1], ans += v[r] - v[l - 1];
			last = r;
		} printf("%lld\n", ans);
	} return 0;
}

植樹 HLP3915

題目描述

\(n\) 個點 \(m\) 條邊,邊從 \(1\)\(m\) 編號。要求把 \([1, m]\) 劃分成儘可能多個區間,使得對於每個區間內的邊,都有一個子集能構成一棵邊權和不超過 \(S\) 的生成樹。

多組數據, \(T \leq 5, 2 \leq n, m \leq 10 ^ 5, s \leq 10 ^ 9。\)

解題思路

如果對每個左端點都進行二分,那麼注意到,每次Check的複雜度是 \(O(k\ log\ k)\) 的,最壞情況下會有 \(O(\frac{m ^ 2}{n}\ log^2\ m)\) 的複雜度,喜提 \(TLE\)

對於這種情況,我們一般有兩種辦法:

  • 數據分塊

  • 改進二分

1. 數據分塊

\(n, m \leq 10 ^ 5\) 的數據範圍下, \(O(\frac{m ^ 2}{n}\ log^2\ m)\) 的複雜度在 \(n\) 較小時就炸掉了,於是考慮針對 \(n\) 的大小進行數據分塊

注意到,如果我們直接往裡面加邊並動態維護最小生成樹,直到形成一個滿足條件的生成樹後把圖清空。這樣的做法是 \(O(nm)\)

至於如何動態維護最小生成樹,可以在加邊時直接暴力求出𝑥,𝑦到𝐿𝐶𝐴上的邊權最大值,然後判斷是否更新,每次更新也可以 \(O(n)\) 進行

可以發現,此時複雜度為 \(min(O(\frac{m ^ 2}{n}\ log^2\ m), O(nm)) \approx m\sqrt{m}\ log\ m\),可以卡過。

2. 改進二分

我們發現,直接二分炸掉的原因是每次二分我們都會直接判斷一個長度為 \(\frac{m}{2}\) 的區間,那麼當答案遠小於這個數值時就會造成極大的時間浪費

於是考慮倍增,先從小的區間開始判斷,然後一直倍增區間的大小直到判斷成功,之後再二分求解。這樣如果對應答案為 \(k\),那麼判斷次數就是 \(O(log\ k)\) 次的

於是對於每個答案是 \(k\) 的區間,花費的複雜度都是 \(O(k \log ^ 2k)\),總的複雜度就是 \(O(m \log ^ 2\ m)\),可以通過本題。

程式碼

#include <bits/stdc++.h>
const int N = 1e5 + 10;
struct node {int u, v, w;};
int n, m, s, fa[N], size[N];
node e[N], g[N];
int get(int x) {return x == fa[x] ? x : fa[x] = get(fa[x]);}
bool check(int l, int r) {
    int ans = 0;
    for (int i = 1; i <= n; ++i) fa[i] = i, size[i] = 1;
    for (int i = l; i <= r; ++i) g[i] = e[i];
    std::sort(g + l, g + r + 1, [&](node a, node b) {return a.w < b.w;});
    for (int i = l; i <= r; ++i) {
        int x = get(g[i].u), y = get(g[i].v);
        if (x != y) {
            int s1 = size[x], s2 = size[y];
            if (s1 < s2) std::swap(x, y);
            fa[y] = x; size[x] += size[y]; ans += g[i].w;
            if (ans > s) return false;
        }
    } int x = get(fa[1]);
    for (int i = 1; i <= n; ++i) if (get(i) != x) return false;
    return true;
}
signed main() {
    int t; scanf("%d", &t);
    while (t--) {
        scanf("%d%d%d", &n, &m, &s);
        for (int i = 1, u, v, w; i <= m; ++i) {
            scanf("%d%d%d", &u, &v, &w);
            e[i] = (node){u, v, w};
        } int ans = 0;
        for (int i = 1, j; i <= m; ++i) {
            for (j = n - 1; i + j - 1 <= m; j <<= 1)
                if (check(i, i + j - 1)) break;
            int l = i, r = std::min(m, i + j - 1);
            if (!check(i, r)) break;
            while (l < r) {
                int mid = l + r >> 1;
                if (check(i, mid)) r = mid;
                else l = mid + 1;
            } ++ ans, i = l;
        } printf("%d\n", ans);
    } return 0;
}

Cow and Fields CF1307D

題目描述

給定一個 \(n\) 個點 \(m\) 條邊的簡單無向連通圖,並指定 \(k\) 個特殊點。要求在特殊點之間加一條邊使得 \(1\)\(n\) 的最短路最大,求對應的最大值。(\(n, m \leq 2 \times 10 ^ 5\)

解題思路

考慮加入一條邊 \(s-t\), 該條邊產生的貢獻為 \(min(dis[1][s] + dis[t][n], dis[1][t] + dis[s][n]) + 1\)

那麼我們只要正反跑兩遍 \(BFS\)(因為邊權都為 \(1\)),處理出每個點到 \(1\)\(n\) 的距離,再將上式取個 \(max\),最後與原先最短路比較一下。

奇技淫巧

考慮欽定 \(dis[1][s] + dis[t][n] < dis[1][t] + dis[s][n]\),這樣通過移項後就可以變成 \(dis[1][s] – dis[s][n] < dis[1][t] – dis[t][n]\)

那麼按照 \(dis(1, i) – dis(n, i)\) 的值進行升序排序,這樣就能保證 \(i < j\)時,連 \(i, j\) 產生的貢獻一定是 \(dis[1][i] + dis[j][n] + 1\),前後綴掃一掃就好了

二分做法

  • 直接二分答案 \(w\)

  • 接下去就是要判斷是否存在 \(s,t\),使得 \(min(dis[1][s] + dis[t][n], dis[1][t] + dis[s][n]) + 1 \geq w\)

  • 枚舉 \(s\),可將問題變成判斷是否存在 \(t\) 使得,\(a_t \geq 𝑤_1, b_t \geq w_2\)

  • 那麼提前按照 \(dis[1][s]\) 的值排好序,就可以保證詢問時 \(𝑤_1\) 是降序的,維護所有滿足 \(a_t \geq w_1\) 的所有 \(t\) 對應的 \(b_t\) 值,類似雙指針的做法不斷插入並用樹狀數組維護即可

  • 時間複雜度 \(O(n\ log ^ 2 ⁡n)\),看似多了個 \(log\) 但實際上跑得飛快。

  • 實際上要維護的只是判斷是否存在 \(b_t \geq w_2\),所以只需要維護最大值,\(O(n\ log\ n)\)即可。

程式碼

#include <bits/stdc++.h>
using pii = std::pair<int, int>;
const int N = 2e5 + 5, inf = 1e9 + 7;
int n, m, k, ans, pre;
int a[N], dis[2][N];
bool vis[N];
std::vector<pii> d;
std::vector<int> Graph[N];
void Bfs(int *dist, int st) {
    std::queue<int> q; q.emplace(st); dist[st] = 0;
    for (int i = 1; i <= n; ++i) vis[i] = false; vis[st] = true;
    while (q.size()) {
        int t = q.front(); q.pop();
        for (auto p : Graph[t])
            if (!vis[p]) {
                vis[p] = true;
                dist[p] = dist[t] + 1;
                q.emplace(p);
            }
    } return void();
}
signed main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= k; ++i) scanf("%d", a + i);
    for (int i = 1, u, v; i <= m; ++i) scanf("%d%d", &u, &v), Graph[u].emplace_back(v), Graph[v].emplace_back(u);
    Bfs(dis[0], 1), Bfs(dis[1], n);
	for (int i = 1; i <= k; ++i) d.emplace_back(pii(dis[0][a[i]] - dis[1][a[i]], a[i]));
    std::sort(d.begin(), d.end()); pre = -inf;
    for (auto it : d) {
        int x = it.second;
        ans = std::max(ans, dis[1][x] + pre);
        pre = std::max(pre, dis[0][x]);
    } return printf("%d\n", std::min(dis[0][n], ans + 1)), 0;
}

Complete The Graph CF715B

題目描述

給定一個無向帶權圖,其中有一些邊的邊權尚未確定。要求確定這些邊的邊權 $ w \in [1, 10 ^{18}]$,使得從 \(s\)\(t\) 的最短路為給定值 \(L\),需要判斷是否有解。

\(n \leq 10 ^ 3, m \leq 10 ^ 4, w_i, L \leq 10 ^ 9\)

解題思路

先跑一遍最短路,對於沒有確定邊權賦為 \(1\), 因為題目要求邊權最少為 \(1\), 此時若最短路長度大於 \(L\) 則肯定無解。

把邊權賦值看作若干次\(+1\)操作。

若將其中一條邊\(+1\),答案影響為 \(0\)\(1\)

那麼我們對操作進行二分,然後跑最短路 \(Check\) 即可。

時間複雜度 \(O(m\ log\ n\ log\ mL)\)

當然這題還有跑兩次最短路的做法,這裡就不再贅述了。

程式碼

#include <bits/stdc++.h>
#define int long long
using pii = std::pair<int, int>;
const int N = 1e4 + 5, inf = 1e18;
int n, m, l, s, t, cnt, tot = 1;
int head[N], dis[N], id[N];
struct edge {int from, to, w, next;}e[N << 1];
void add(int u, int v, int w) {
    e[++ tot] = (edge){u, v, w, head[u]}; head[u] = tot;
    e[++ tot] = (edge){v, u, w, head[v]}; head[v] = tot;
    return void();
}
void dijkstra(int s) {
    static bool vis[N];
    std::priority_queue<pii, std::vector<pii>, std::greater<pii>> q;
    std::memset(dis, 0x3f, sizeof dis);
    std::memset(vis, false, sizeof vis);
    q.emplace(pii(0, s)); dis[s] = 0;
    while (q.size()) {
        pii t = q.top(); q.pop();
        int u = t.second;
        if (vis[u]) continue; vis[u] = true;
        for (int i = head[u]; i; i = e[i].next)
            if (dis[u] + e[i].w < dis[e[i].to]) q.emplace(pii(dis[e[i].to] = dis[u] + e[i].w, e[i].to));
    } return void();
}
int calc(int x, int i) {
    int p = x / cnt;
    x -= p * cnt;
    return i <= x ? p + 2 : p + 1;
}
bool check(int mid) {
    for (int i = 1; i <= cnt; ++i) e[id[i]].w = e[id[i] ^ 1].w = calc(mid, i); 
    dijkstra(s);
    return dis[t] >= l;
}
signed main() {
    scanf("%lld%lld%lld%lld%lld", &n, &m, &l, &s, &t);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%lld%lld%lld", &u, &v, &w);
        if (!w) id[++ cnt] = tot + 2, ++ w;
        add(u, v, w);
    } dijkstra(s);
    if (dis[t] > l) return puts("NO"), 0;
    int le = 0, ri = l * m, ans = 1;
    while (le < ri) {
        int mid = le + ri >> 1;
        if (check(mid)) ri = mid;
        else le = mid + 1;
    } if (!check(le)) return puts("NO"), 0;
    puts("YES");
    for (int i = 1; i <= cnt; ++i) e[id[i]].w = calc(le, i);
    for (int i = 1; i <= m; ++i) printf("%lld %lld %lld\n", e[i << 1].from, e[i << 1].to, e[i << 1].w);
    return 0;
}

森林 HLP3469

題目描述

給定一個 \(n\) 個點 \(m\) 條邊的無向圖,每條邊有一個邊權。有 \(k\) 次操作,每次操作會選擇盡量多的邊刪除,如果有多種操作會選擇邊權和最大的那個,但是要求刪除的邊構成一個森林。對於每條邊,輸出它在第幾次操作時被刪除。

\(n \leq 10 ^ 3, m \leq 3 \times 10 ^ 5, k \leq 10^4\)

解題思路

要求刪除的邊構成森林,意思就是不能有環。

不難想到求 \(k\) 次最大生成樹,不過這樣時間複雜度是 \(O(km\ log\ m)\) 喜提 \(TLE\)

鏈表

注意到第 \(𝑖\) 條邊被用過之後就不會再被用了,於是考慮對邊排完序後建一個鏈表,每次選一條邊後就把他刪掉,模擬即可,相對難寫

k層並查集

可以建 \(k\) 層並查集,每次到第 \(i\) 條邊的時候直接二分出這條邊被加在哪一層,相對好寫。

程式碼

#include <bits/stdc++.h>
const int N = 1e3 + 5, M = 3e5 + 5, K = 1e4 + 5;
int n, m, k, cnt, head[N], ans[M], fa[K][N];
struct edge{int u, v, w, id, next;} e[M << 1];
void add(int u, int v, int w, int id) {e[++ cnt] = (edge){u, v, w, id, head[u]}; head[u] = cnt;}
int get(int layer, int x) {return fa[layer][x] == x ? x : fa[layer][x] = get(layer, fa[layer][x]);}
signed main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
		add(u, v, w, i);
    }
    for (int i = 1; i <= k; ++i) for (int j = 1; j <= n; ++j) fa[i][j] = j;
    std::sort(e + 1, e + m + 1, [&](edge a, edge b) {return a.w > b.w;});
    for (int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v;
        int l = 1, r = k + 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (get(mid, u) ^ get(mid, v)) r = mid;
            else l = mid + 1;
        }
        if (l == k + 1) continue;
        ans[e[i].id] = l;
        u = get(l, u), v = get(l, v);
        fa[l][u] = v;
    } for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]); 
    return 0;
}

Hemose in ICPC CF1592D

Tags: