20240624

forgotmyhandle發表於2024-06-26

T1

NFLSOJ P955 數字收藏

首先除掉 \(k\),變成加入刪除一個數,查詢集合中有多少數與加入的數互質。直接考慮容斥,查詢時把查詢數的所有本質不同質因數拎出來,二進位制列舉一下做容斥,加入刪除也同理二進位制列舉質因數做加入刪除。

程式碼
#include <iostream>
#include <vector>
#define int long long
using namespace std;
const int N = 100000;
int n, K;
bool pr[100005];
int p[100005], pcnt;
int pnum[100005];
void Sieve(int n) {
    for (int i = 2; i <= n; i++) {
        if (!pr[i]) 
            p[++pcnt] = i, pnum[i] = 1;
        for (int j = 1; j <= pcnt && 1ll * p[j] * i <= n; j++) {
            pr[p[j] * i] = 1;
            pnum[p[j] * i] = pnum[i] + 1;
            if (i % p[j] == 0) 
                break;
        }
    }
}
vector<int> pfac[100005];
vector<int> fct[100005];
vector<int> asdf[100005];
int ap[100005];
int cm[100005];
int ans = 0;
int ncnt;
void Add(int x, int y) {
    if (x % K != 0) 
        return;
    x /= K;
    if (y < 0) {
        for (auto v : fct[x]) 
            cm[v] += y;
    }
    int tmp = 0;
    for (auto v : asdf[x]) {
        if (pnum[v] & 1) 
            tmp -= cm[v];
        else 
            tmp += cm[v];
    }
    ans += tmp * y;
    if (y > 0) {
        for (auto v : fct[x]) 
            cm[v] += y;
    }
}
signed main() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);
    cin >> n >> K;
    Sieve(100000);
    for (int i = 1; i <= pcnt; i++) {
        for (int j = 1; p[i] * j <= N; j++) 
            pfac[p[i] * j].emplace_back(p[i]);
    }
    for (int i = 1; i <= N; i++) {
        int s = pfac[i].size();
        int S = (1 << s) - 1;
        for (int j = 0; j <= S; j++) {
            int tmp = 1;
            for (int k = 0; k < s; k++) {
                if (j & (1 << k)) 
                    tmp *= pfac[i][k];
            }
            asdf[i].emplace_back(tmp);
        }
        for (int j = 1; j * j <= i; j++) {
            if (i % j == 0) {
                fct[i].emplace_back(j);
                if (j * j != i) 
                    fct[i].emplace_back(i / j);
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        int x, y;
        cin >> x >> y;
        if (x == 0) {
            if (ap[y]) {
                Add(y, -1);
                --ap[y];
            }
        } else {
            ++ap[y];
            Add(y, 1);
        }
        cout << ans << "\n";
    }
    return 0;
}

T2

NFLSOJ P2697 樹形猜猜看

先考慮隨一個點,把這個點對所有點問一遍。容易發現這樣可以確定這個點到根的路徑上所有點及其子樹內有哪些點。這樣就可以遞迴,但是複雜度不是很保證。如果每次都能隨到葉子,則可以證明詢問次數:設 \(f(i)\) 為層數為 \(i\) 的二叉樹每次照著葉子問最少的次數,可以得到 \(f(1) = 1, f(n) = 2^n - 2 + \sum_{i = 1}^{n - 1}f(i)\)。遞推計算可以得到 \(f(10) < 4800\),因此複雜度有保證。接下來考慮如何找到葉子。注意,我們需要在找葉子的過程中融入詢問所有點的過程,並求出找到的葉子到根的路徑,否則還是無法保證複雜度。考慮對於當前節點 \(cur\),初始令為根,列舉所有節點進行詢問,設詢問點為 \(v\),詢問答案為 \(a\),若 \(a = cur\),說明 \(v\)\(cur\) 的子樹當中,直接 \(cur \leftarrow v\),若 \(a = v\),說明 \(v\) 一定在將要找到的葉子到根的路徑上,否則 \(a\) 在將要找到的葉子到根的路徑上,而 \(v\)\(a\) 的子樹中。這樣將所有點都問過一遍之後,一定可以找到葉子,而路徑上的點也能全部求出,每個點的子樹也能獲得。完美符合要求,直接分成子問題繼續遞迴即可。

程式碼
#include <bits/stdc++.h>
using namespace std;
random_device rd;
mt19937 mtrand(rd());
int n;
bool mark[100005];
int query(int x, int y) {
    cout << "? " << x << " " << y << endl;
    int ret;
    cin >> ret;
    return ret;
}
int ans[1050];
int work(int x, vector<int> vec) {
    if (vec.size() == 1) 
        return x;
    int stk[1050], sz = 0;
    vector<int> sbt[1050];
    int cur = x;
    stk[++sz] = x;
    for (auto v : vec) {
        if (v == x) 
            continue;
        int tmp = query(cur, v);
        if (tmp == cur) {
            stk[++sz] = v;
            cur = v;
            continue;
        }
        stk[++sz] = tmp;
        if (tmp != v) 
            sbt[tmp].emplace_back(v);
    }
    sort(stk + 1, stk + sz + 1, [&](int a, int b) { return sbt[a].size() < sbt[b].size(); });
    sz = unique(stk + 1, stk + sz + 1) - stk - 1;
    for (int i = 1; i < sz; i++) ans[stk[i]] = stk[i + 1];
    for (int i = 1; i <= sz; i++) {
        if (sbt[stk[i]].size()) 
            ans[work(sbt[stk[i]][0], sbt[stk[i]])] = stk[i];
    }
    return stk[sz];
}
vector<int> asdf;
int main() {
    // freopen("B.in", "r", stdin);
    // freopen("B.out", "w", stdout);
    cin >> n;
    for (int i = 1; i < (1 << n); i++) asdf.emplace_back(i);
    ans[work(1, asdf)] = -1;
    cout << "! ";
    for (int i = 1; i < (1 << n); i++) cout << ans[i] << " ";
    cout << endl;
    return 0;
}

T3

NFLSOJ P2699 修得大樹千萬

先考慮對於一個點 \(u\) 如何做。刪去 \(u\) 後,假設分裂成 \(m\) 個連通塊,則一共會連出 \(m - 1\) 條邊,也就是選出 \(2m - 2\) 個點,其中有 \(m\) 個點要求來自兩兩不同的連通塊,一個點可以重複選很多次。容易發現對於任意的這樣的選點方案一定有一種對應的連邊方案使得最終連通。於是只需要求出當前點每個子樹中的最大值以及子樹外最大值,然後再選出 \(m - 2\) 個點即可。由於每個點的度數之和是 \(O(n)\) 級別的,所以每個點選出的點數也是這一級別。因此選點可以暴力做。子樹外最大值只需要前字尾維護一下即可。使用 multiset 維護選數,則選中一個數即為刪除這個數,加入這個數 \(+1\)。一個點搞完之後要回滾初始化。

程式碼
#include <iostream>
#include <algorithm>
#include <vector>
#include <set>
#define int long long
using namespace std;
inline char nnc(){
    static char buf[1000005],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1000005,stdin),p1==p2)?EOF:*p1++;
}
inline int read() {
    int ret = 0;
    char c = nnc();
    while (c < '0' || c > '9') c = nnc();
    while ('0' <= c && c <= '9') ret = ret * 10 + c - 48, c = nnc();
    return ret;
}
int n;
int a[1000005];
vector<int> vec[1000005];
vector<int> pre[1000005];
vector<int> suf[1000005];
int mn[1000005];
vector<int> val[1000005];
vector<int> son[1000005];
void dfs1(int x, int fa) {
    mn[x] = a[x];
    for (auto v : vec[x]) {
        if (v != fa) {
            dfs1(v, x);
            son[x].emplace_back(v);
            mn[x] = min(mn[x], mn[v]);
            val[x].emplace_back(mn[v]);
            pre[x].emplace_back(mn[v]);
            suf[x].emplace_back(mn[v]);
        }
    }
    for (int i = 1; i < (int)pre[x].size(); i++) pre[x][i] = min(pre[x][i], pre[x][i - 1]);
    for (int i = (int)suf[x].size() - 2; i >= 0; i--) suf[x][i] = min(suf[x][i], suf[x][i + 1]);
}
void dfs2(int x, int fa, int val) {
    if (val <= n) 
        ::val[x].emplace_back(val);
    val = min(val, a[x]);
    for (int i = 0; i < (int)son[x].size(); i++) {
        int v = son[x][i];
        if (son[x].size() == 1) 
            dfs2(v, x, val);
        else {
            int tmp = 2147483647;
            if (i == 0) 
                tmp = suf[x][i + 1];
            else if (i == (int)son[x].size() - 1) 
                tmp = pre[x][i - 1];
            else 
                tmp = min(pre[x][i - 1], suf[x][i + 1]);
            dfs2(v, x, min(val, tmp));
        }
    }
}
int stk[1000005], sz;
int deg[1000005];
multiset<int> st;
int calc(int x) {
    if (deg[x] <= 1) 
        return 0;
    int ret = 0;
    sz = 0;
    st.erase(st.find(a[x]));
    for (auto v : val[x]) {
        stk[++sz] = v;
        ret += v;
        st.erase(st.find(v));
        st.insert(v + 1);
    }
    int cnt = deg[x] - 2;
    while (cnt--) {
        int v = *st.begin();
        ret += v;
        stk[++sz] = v;
        st.erase(st.begin());
        st.insert(v + 1);
    }
    st.insert(a[x]);
    for (int i = sz; i; i--) {
        st.erase(st.find(stk[i] + 1));
        st.insert(stk[i]);
    }
    return ret;
}
signed main() {
    // freopen("C.in", "r", stdin);
    // freopen("C.out", "w", stdout);
    n = read();
    for (int i = 1; i <= n; i++) st.insert(a[i] = read());
    for (int i = 1; i < n; i++) {
        int u, v;
        u = read(), v = read();
        vec[u].emplace_back(v);
        vec[v].emplace_back(u);
        ++deg[u], ++deg[v];
    }
    dfs1(1, 0);
    dfs2(1, 0, 2147483647);
    for (int i = 1; i <= n; i++) cout << calc(i) << " ";
    cout << "\n";
    return 0;
}

T4

NFLSOJ P5353 公益

先把所有同學按開始時間排序。如果把每個人吃的時間在數軸上標出,那麼可以發現在食物補給前被殺死的一定是從補給時刻向前推的一段區間內的人。由於小 X 不能死,所以若排序後兩個相鄰的數是 \(x_i, x_j\),那麼所有滿足 \(x_i < S_a \bmod t < x_j\) 的商販 \(a\) 都是本質相同的,因為他們能殺死的人完全相同。根據貪心的原則,如果確定了一個人要殺掉,則一定是越早殺掉越好。我們考慮一個 dp:\(dp[i]\) 表示考慮了排序後的前 \(i\) 個同學的最小代價,若存在 \(k\) 使得 \(S_k \bmod t \in (x_i, x_{i + 1})\),則轉移考慮列舉 \(j\),令 \((j, i]\) 之內的人被殺掉,根據這些人的死亡時間、活著時帶來的代價以及精神損失費,可以列出一種轉移。然後如果令 \(i\) 不被殺掉,則直接從 \(f_{i - 1}\) 轉移。列出式子可以發現第一種轉移可以使用斜率最佳化,於是拿棧維護一下直線,轉移時二分即可。

程式碼
#include <iostream>
#include <algorithm>
#define int __int128
using namespace std;
const int inf = 2147483647;
long long T, n, m, w, t;
struct Person {
    long long x, c;
} a[200005];
struct Merchant {
    long long S, v;
} b[200005];
int Min[200005];
struct Line {
    int k, b;
    int operator()(int x) { return k * x + b; }
} stk[200005];
int f[200005], S[200005];
int sz;
bool chk(Line a, Line b, Line c) { return (1.0 * b.b - a.b) * (b.k - c.k) >= (1.0 * c.b - b.b) * (a.k - b.k); }
int Search(int x) {
    int l = 1, r = sz - 1, ans = sz, mid;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (stk[mid + 1](x) >= stk[mid](x)) 
            ans = mid, r = mid - 1;
        else 
            l = mid + 1;
    }
    return ans;
}
signed main() {
    // freopen("D.in", "r", stdin);
    // freopen("D.out", "w", stdout);
    cin >> T >> n >> m >> w >> t;
    for (int i = 1; i <= n; i++) cin >> b[i].S, b[i].v = b[i].S / t, b[i].S %= t;
    b[++n] = (Merchant) { T % t, T / t };
    for (int i = 1; i <= m; i++) cin >> a[i].x >> a[i].c;
    for (int i = 1; i <= m; i++) Min[i] = inf;
    sort(a + 1, a + m + 1, [](Person a, Person b) { return a.x < b.x; });
    sort(b + 1, b + n + 1, [](Merchant a, Merchant b) { return a.S < b.S; });
    a[m + 1].x = inf;
    for (int i = 1, j = 1; i <= m; i++) {
        S[i] = S[i - 1] + a[i].c;
        while (j <= n && b[j].S <= a[i].x) ++j;
        while (j <= n && b[j].S < a[i + 1].x) Min[i] = min(Min[i], (int)b[j].v), ++j;
    }
    stk[++sz] = (Line) { 0, 0 };
    for (int i = 1; i <= m; i++) {
        f[i] = f[i - 1] + w * ((T - a[i].x) / t + 1);
        if (Min[i] != inf) {
            int x = w * Min[i];
            int t = Search(x);
            f[i] = min(f[i], stk[t](x) + w * Min[i] * i + S[i]);
        }
        Line tmp = (Line) { -i, f[i] - S[i] };
        while (sz > 1 && chk(stk[sz - 1], stk[sz], tmp)) --sz;
        stk[++sz] = tmp;
    }
    cout << (long long)(f[m] + w * (T / t + 1)) << "\n";
    return 0;
}

連邊使連通有時可以轉化為選出一些點的方案,使得每個連通塊中都至少有一個點被選中。