雙指針

CSES - Sum of Two Values

給一個長度為 n 的序列 \(a_1, \ldots ,a_n\),以及數字 x,問這 n 個數字中哪兩個數字和為 x,輸出任何一組解

\(n\le 2\times 10^5, 1\le x,a_i\le 10^9\)

思路

排序後使用雙指針

code
void solve() {
    vector<pair<int, int>> v;
    int n, x;
    cin >> n >> x;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        v.push_back({x, i});
    }
    sort(v.begin(), v.end());
    int l = 0, r = n - 1;
    while (r > l) {
        if (v[l].first + v[r].first > x) {
            r--;
        } else if (v[l].first + v[r].first < x) {
            l++;
        } else {
            cout << v[l].S << ' ' << v[r].S << endl;
            exit(0);
        }
    }
    cout << "IMPOSSIBLE\n";
}
CSES - Sum of Three Values

給一個長度為 n 的序列 \(a_1, \ldots ,a_n\),以及數字 x,問這 n 個數字中哪三個數字和為 x,輸出任何一組解

\(n\le 5000, 1\le x, a_i\le 10^9\)

思路

枚舉第一項,後續套用 Sum of Two Values

Sum of Two values 變化

給一個長度為 n 的序列 \(a_1, \ldots ,a_n\),以及數字 x,問有幾組 (i, j) 使 \(a_i+a_j=x\)

\(n\le 2\times 10^5, 1\le x, a_i\le 10^9\)

思路

開一個桶 cnt[ ] 紀錄每種數字出現的次數,然後我們就可以將 a[ ] sort,並使用 unique 去除重複的元素,然後用雙指針(枚舉 l,r 會單調遞減)

code
1
2
3
4
5
6
7
8
9
int j = n - 1;
for (int i = 0; i < n; i++) {
    while (i < j && a[i] + a[j] >= x) {
        if (a[i] + a[j] == x) {
            ans += cnt[i] * cnt[j];
        }
        j--;
    }
}
USACO 2021 December Contest, Silver Problem 2. Connecting Two Barns

\(n\)\(m\) 邊,點邊號 \(1\ldots n\),可以額外建最多兩條邊,在點 \(i,j\) 之間建邊花費 \((i-j)^2\),問最小花費使點 \(1\) 跟點 \(n\) 連通

\(n,m\le 10^5\)

思路

對於 1 和 n 有兩種情況:

  1. 在同一個連通塊裡。
  2. 在兩個不同的連通塊裡。

接下來用 f[i] 來表示 i 所在的連通塊的代表點。對於情況 1,很明顯不需要連任何一邊,所以花費為 0。對於情況 2,一定是一個在 f[1],連到中間的連通塊 f[i],再從 f[i] 連到 f[n]。我們去預處理 f[1] 到 f[i] 的最近距離,存在 cost1[f[i]],也去去預處理 f[i] 到 f[n] 的最近距離,存在 costn[f[i]]。最後的答案就是 cost1[i] + costn[n] 取最小值。至於要怎麼預處理有兩種方法,第一種是二分,也就是假設要算 f[i] 到 f[j] 的最小距離,先枚舉 f[i] 內的點,假設叫 k,我們就在 f[j] 內二分第一個比 k 大的數字與第一個比 k 小的數字,取 min 即可。第二種方法是 two pointer,我們去枚舉一個點 i,對於這個 i 一定是想要選數值越接近自己的點越好,假設現在是看到 f[1] 的距離,就是用 two pointer 維護在 f[1] 第一個比 i 大的數字與第一個比 i 小的數字(見代碼 line 54 ~ 72),最後把這個更新在 cost1[f[i]] 即可。

講不清楚可以參考官方詳解

code(from usaco)
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;

void dfs(const vector<vector<int>> &G, vector<int> &cc, const int u, const int id) {
    for (int v : G[u]) {
        if (cc[v] != id) {
            cc[v] = id;
            dfs(G, cc, v, id);
        }
    }
}

void solve() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> G(n);
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        a--;
        b--;
        G[a].push_back(b);
        G[b].push_back(a);
    }

    vector<int> cc(n);
    iota(cc.begin(), cc.end(), 0);
    for (int i = 0; i < n; i++) {
        if (cc[i] == i) {
            dfs(G, cc, i, i);
        }
    }

    if (cc[0] == cc[n - 1]) {
        cout << "0\n";
        return;
    }

    vector<vector<int>> st(n);
    for (int i = 0; i < n; i++) {
        st[cc[i]].push_back(i);
    }

    long long ans = 1e18;
    vector<long long> cost1(n, 1e9);
    vector<long long> costn(n, 1e9);
    int idx1 = 0;
    int idxn = 0;
    for (int i = 0; i < n; i++) {
        while (idx1 < st[cc[0]].size()) {
            cost1[cc[i]] = min(cost1[cc[i]], (long long)abs(i - st[cc[0]][idx1]));
            if (st[cc[0]][idx1] < i) {
                idx1++;
            } else {
                break;
            }
        }
        if (idx1) idx1--;

        while (idxn < st[cc[n - 1]].size()) {
            costn[cc[i]] = min(costn[cc[i]], (long long)abs(i - st[cc[n - 1]][idxn]));
            if (st[cc[n - 1]][idxn] < i) {
                idxn++;
            } else {
                break;
            }
        }
        if (idxn) idxn--;
    }

    for (int i = 0; i < n; i++) {
        ans = min(ans, cost1[i] * cost1[i] + costn[i] * costn[i]);
    }

    cout << ans << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;

    for (int i = 0; i < t; i++) {
        solve();
    }

    return 0;
}
USACO 2013 JAN Cow Lineup G

有 n 頭牛排成一列,其中第 i 個的品種是 a[i]。只能刪掉至多 k 種品種的情況下,問品種相同的連續段的最大長度

\(n\le 10^5, a_i \le 10^9\)

思路

可以把題目看成: 對於每個有 k + 1 種品種的 subarray,問同種種類最多可以是多少。

最暴力的想法就是枚舉右界 r,然後暴力的找到 l 使 [l, r] 恰有 k + 1 種品種,用 r 的品種來更新答案。但我們可以發現,這種 subarray 具有單調性,我們可以用 two pointer 維護,詳見代碼。

code
#include <iostream>
#include <map>

using namespace std;

const int N = 100005;

int a[N];

int main() {
    int n, k;
    cin >> n >> k;

    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    map<int, int> mp;
    int ans = 0;
    int l = 1;
    for (int r = 1; r <= n; ++r) {
        ++mp[a[r]];

        while (mp.size() > k + 1) {
            --mp[a[l]];
            if (mp[a[l]] == 0) {
                mp.erase(a[l]);
            }
            ++l;
        }

        ans = max(ans, mp[a[r]]);
    }

    cout << ans;
}
USACO 2019 FEB Sleepy Cow Herding S

一維數線上有 n 頭牛,每次只能挪動 edge point(最右邊或最左邊)的牛到任意位置,不過不能使他移動後還是在 edge point,問讓這些牛完全相鄰的最少和最多挪動次數。

\(n\le 10^5\)

思路

對於第一個問題求最小操作次數,由於每一步操作都在佔領空位,而最終狀態為一段包含連續 \(n\) 個位置的區間,所以可以從結果出發,用 two pointer,枚舉這個最終區間的左端點 \(a_i\),找到右端點長度 \(a_j\),使 \(a_j-a_i+1\le n\),這段區間的答案就會是 n - 區間內的牛的數量(將外面的牛都移進來這個區間內),即 n - (i - j + 1)。可以看一下下圖的移動方式。

Image title

因為我們 edge point 移動過去之所以合法是因為我們能找到中間的空格,或者是另一端也有 edge point,可以讓牛安心的過去另一端 edge point 的旁邊。如果兩者都沒有,就會是以下兩種特殊情況。如果前 \(n-1\) 個位置緊鄰,而最後一個位置離倒數第二個位置距離大於 \(2\),比如 \(1,2,3,4,7\),答案應為 \(2\)。同理,如果後 \(n-1\) 個位置緊鄰,而第一個位置離第二個位置距離大於 \(2\),答案也應為 \(2\)

Image title

因為不能從 edge point 還到 edge point ,所以會比較類似一個區間一直在縮小(一個大的區間縮小成為一個長度為 n 的區間)。由於要讓移動次數越大越好,所以我們要盡量慢慢移動,而收攏的區間一定是越大越好,所以我們可以朝最左邊慢慢收攏過去,或是朝最右邊慢慢收攏過去。假如現在是朝最左邊慢慢收攏過去,我們一開始先將第 n 頭牛移動到區間 [a[1], a[n - 1]] 的最右邊的空格,這樣才不會讓他成為 edge point,然後再來我們就只要計算 [a[1], a[n - 1]] 內空格的數量,就可以得知接下來的操作次數。假如區間是 [l, r](在這邊 l = a[1], r = a[n - 1]),答案也就是區間長度 - 區間內的牛的數量 + 一開始第 n 頭牛移過去的一次操作 = (r - l + 1) - n + 1。同理朝最右邊慢慢收攏過去就是 l = a[2], r = a[n]。

Image title

code
#include <bits/stdc++.h>
using namespace std;

int n, a[100005], ans, ans2;

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a + 1, a + n + 1);
    if ((a[n - 1] - a[1] == n - 2 && a[n] - a[n - 1] > 2) || (a[n] - a[2] == n - 2 && a[2] - a[1] > 2)) {
        ans = 2;  // 特判
    } else {
        int j = 1, res = 0;
        for (int i = 1; i <= n; i++) {
            while (j < n && a[j + 1] - a[i] + 1 <= n) {
                j++;
            }
            res = max(res, j - i + 1);
        }
        ans = n - res;
    }
    cout << ans << '\n';
    cout << max(a[n - 1] - a[1], a[n] - a[2]) - n + 2 << '\n';
    return 0;
}
自編題

給一個長度為 \(n\) 的序列,第 \(i\) 項有兩個數值 \((a_i,b_i)\)。問有幾組 \((i,j)\) 滿足 \(a_i+b_j\le t\),注意 \((i,j)\)\((j,i)\) 是同一種方案。

\(n\le 10^5,1\le a_i,b_i,t\le 10^9\)

思路

我們可以先算出來全部的方法數(包含 \((i,j),(j,i)\) 都算進去),然後再扣掉同時滿足 \(a_i+b_j\le t,a_j+b_i\le t\) 的方案。這種方法數都可以透過下面的方法算出來。我們可以將式子移向寫成 \(b_j\le t-a_i\),我們讓 \(a_i\) 從小到大排序,這樣合法的 \(b_j\) 會越來越少。我們將 \(b_j\) 排序好後放在一個陣列,我們枚舉 \(i\),因為具有單調性,所以我們可以慢慢拓展滿足 \(b_j\le t-a_i\) 的後綴區間,相當於讓一個掃描線從右邊往左掃過來,在這裡就可以維護全部的方法數(對於每個 i,ans += j)。現在的問題就變成說要怎麼看滿足 \(b_j\le t-a_i\) 的這些 \(b_j\),滿足 \(b_i\le t - a_j\) 的有幾個。我們維護一個資料結構 DS,假設目前跑過的 \(i\)\(b_i\) 都已經加入 DS,那每掃過一個 \(b_j\) 要在 DS 內看的就是有幾個 \(b_i\le t - a_j\)。注意到這樣也會重複計算,不過我們只要扣掉 \(a_i + b_i \le t\) 的這種情況後,就可以將方法數除 2 即可。所以最後答案就是「全部的法數」 - 「重複的方法數(扣掉 a[i] + b[i] <= t) / 2 」。這裡給出一個例子

Image title

code
#include <bits/extc++.h>
#include <bits/stdc++.h>
using namespace std;
using namespace __gnu_pbds;
template <typename T>
using rank_set = tree<T, null_type, less<T>, rb_tree_tag, tree_order_statistics_node_update>;

struct DS {
    rank_set<int> st;
    void insert(int x) {
        st.insert(x);
    }
    int query(int x) {
        return st.order_of_key(x + 1);
    }
};

struct Node {
    int a, b;
};

bool cmp_a(Node x, Node y) {
    return x.a < y.a;
}

bool cmp_b(Node x, Node y) {
    return x.b < y.b;
}

const int MAXN = 1e5 + 5;
Node va[MAXN], vb[MAXN];

signed main() {
    int n, t;
    cin >> n >> t;
    for (int i = 1; i <= n; i++) {
        cin >> va[i].a;
        vb[i].a = va[i].a;
    }
    for (int i = 1; i <= n; i++) {
        cin >> va[i].b;
        vb[i].b = va[i].b;
    }
    DS ds;
    sort(va + 1, va + n + 1, cmp_a);
    sort(vb + 1, vb + n + 1, cmp_b);
    int j = n, ans1 = 0, ans2 = 0;
    for (int i = 1; i <= n; i++) {
        // 枚舉 i, 維護掃描線使 b[j] <= t - a[i]
        while (1 <= j && vb[j].b > t - va[i].a) {
            // 重複的方法數計算
            ans2 += ds.query(t - vb[j].a);
            j--;
        }
        ans1 += j; // 全部的方法數
        ds.insert(va[i].b);
    }
    // 最後還每跑到的 j 也要記得結算
    while (1 <= j) {
        // 重複的方法數計算
        ans2 += ds.query(t - vb[j].a);
        j--;
    }
    for (int i = 1; i <= n; i++) {
        if (va[i].a + va[i].b <= t) {
            ans2--;
        }
    }
    cout << ans1 - (ans2 / 2) << '\n';
}

/*
3 5
2 3 4
2 1 3
*/