20240624

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)=2n2+i=1n1f(i)。递推计算可以得到 f(10)<4800,因此复杂度有保证。接下来考虑如何找到叶子。注意,我们需要在找叶子的过程中融入询问所有点的过程,并求出找到的叶子到根的路径,否则还是无法保证复杂度。考虑对于当前节点 cur,初始令为根,枚举所有节点进行询问,设询问点为 v,询问答案为 a,若 a=cur,说明 vcur 的子树当中,直接 curv,若 a=v,说明 v 一定在将要找到的叶子到根的路径上,否则 a 在将要找到的叶子到根的路径上,而 va 的子树中。这样将所有点都问过一遍之后,一定可以找到叶子,而路径上的点也能全部求出,每个点的子树也能获得。完美符合要求,直接分成子问题继续递归即可。

代码
#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 个连通块,则一共会连出 m1 条边,也就是选出 2m2 个点,其中有 m 个点要求来自两两不同的连通块,一个点可以重复选很多次。容易发现对于任意的这样的选点方案一定有一种对应的连边方案使得最终连通。于是只需要求出当前点每个子树中的最大值以及子树外最大值,然后再选出 m2 个点即可。由于每个点的度数之和是 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 不能死,所以若排序后两个相邻的数是 xi,xj,那么所有满足 xi<Samodt<xj 的商贩 a 都是本质相同的,因为他们能杀死的人完全相同。根据贪心的原则,如果确定了一个人要杀掉,则一定是越早杀掉越好。我们考虑一个 dp:dp[i] 表示考虑了排序后的前 i 个同学的最小代价,若存在 k 使得 Skmodt(xi,xi+1),则转移考虑枚举 j,令 (j,i] 之内的人被杀掉,根据这些人的死亡时间、活着时带来的代价以及精神损失费,可以列出一种转移。然后如果令 i 不被杀掉,则直接从 fi1 转移。列出式子可以发现第一种转移可以使用斜率优化,于是拿栈维护一下直线,转移时二分即可。

代码
#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;
}

连边使连通有时可以转化为选出一些点的方案,使得每个连通块中都至少有一个点被选中。

posted @   forgotmyhandle  阅读(19)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示