Loading

2023年度好题(3)

文章有点长,都是由本人一点一点写出来的,公式加载需要一段时间。

CF576D Flights for Regular Customers

思路

首先我们可以按边的权值对边从小到大进行排序。

然后,我们可以从前往后枚举每一条边 \(i\),假设边 \(i\) 需要经过至少 \(d_i\) 条边才能经过这条边,我们就需要将 \(1\sim i - 1\) 这些边建成一个图然后看一看走 \(d_i\) 步能到达的点有哪些,这是一道经典的矩阵题目,不会的可以看看P2886

因为我们已经走了 \(d_i\) 条边了,我们可以将边 \(i\) 放入图中,然后我们以从 \(1\)\(d_i\) 步可以到达的点为源点在新图上进行 bfs,看能不能到 \(n\) 点,如果能,那么用 \(dis_n + d_i\) 更新答案,表示我要先走 \(d_i\) 步加入了边 \(i\),再走 \(dis_n\) 步就到了 \(n\)

同时要注意,这个题目的矩阵乘法不进行优化会 TLE,那么我们怎么优化呢?

看以下代码:

for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
        for (int k = 1; k <= n; k++)
            res.a[i][j] |= (a.a[i][k] & b.a[k][j]);

我们发现 ja.a[i][k] 毫无关系,所以当 a.a[i][k] 成立时,整个 b.a[k] 都可以与 res.a[i] 进行异或,所以这一步就交给 bitset 来做就好了。

for (int i = 1; i <= n; i++)
    for (int k = 1; k <= n; k++)
        if (a.a[i][k])
            res.a[i] |= b.a[k];

代码

/*******************************
| Author:  SunnyYuan
| Problem: Flights for Regular Customers
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/CF576D
| When:    2023-10-09 14:46:28
| 
| Memory:  250 MB
| Time:    4000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 160, INF = 0x3f3f3f3f;

struct matrix {
    bitset<N> a[N];
} g, ans;

int n, m;

matrix operator*(matrix a, matrix b) {
    matrix res;
    memset(res.a, 0, sizeof(res.a));
    for (int i = 1; i <= n; i++)
        for (int k = 1; k <= n; k++)
            if (a.a[i][k])
                res.a[i] |= b.a[k];
    return res;
}

void pow(matrix& ans, matrix a, int b) {
    while (b) {
        if (b & 1) ans = ans * a;
        a = a * a;
        b >>= 1;
    }
}

struct edge {
    int u, v, w;
} e[N];

int d[N];
queue<int> q;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= m; i++) cin >> e[i].u >> e[i].v >> e[i].w;
    sort(e + 1, e + m + 1, [](const edge e1, const edge e2) { return e1.w < e2.w; });
    memset(ans.a, 0, sizeof(ans.a));
    for (int i = 1; i <= n; i++) ans.a[i][i] = 1;
    int pass = 0, res = INF;
    for (int i = 1; i <= m; i++) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        int take = w - pass;
        pow(ans, g, take);
        g.a[u][v] = 1;

        memset(d, 0x3f, sizeof(d));
        for (int i = 1; i <= n; i++){
            if (ans.a[1][i]) d[i] = 0, q.push(i);
        }

        while (q.size()) {
            int t = q.front();
            q.pop();

            for (int i = 1; i <= n; i++)
                if (g.a[t][i] && d[i] == INF) {
                    d[i] = d[t] + 1;
                    q.push(i);
                }
        }
        pass = w;
        res = min(res, pass + d[n]);
    }
    if (res == INF) cout << "Impossible\n";
    else cout << res << '\n';
    return 0;
}

CF915D Almost Acyclic Graph

思路

非常有意思的一道拓扑排序题。

如果图中没有环,那么在拓扑排序后所有的入度都小于等于 \(0\)

如果去掉一条边 \((u, v)\),那么就相当于 \(v\) 的入度 \(-1\)

所以我们相当于枚举 \(i = 1\sim n\),将 \(i\) 的入度 \(-1\) 并且重新跑一遍拓扑排序,如果入度都小于等于 \(0\),那么输出证明确实能够去掉最多一条边就让这个图无环。

代码

/*******************************
| Author:  SunnyYuan
| Problem: Almost Acyclic Graph
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF915D
| When:    2023-10-13 17:34:10
| 
| Memory:  250 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 510;

int n, m;
vector<int> e[N];
int in[N], back[N];

bool topsort() {
    memcpy(in, back, sizeof(in));
    queue<int> q;
    for (int i = 1; i <= n; i++) if (!in[i]) q.push(i);
    while (q.size()) {
        int t = q.front();
        q.pop();
        for (int to : e[t]) {
            in[to]--;
            if (!in[to]) q.push(to);
        }
    }
    for (int i = 1; i <= n; i++) if (in[i] > 0) return false;
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        e[u].push_back(v);
        back[v]++;
    }
    if (topsort()) {
        cout << "YES\n";
        return 0;
    }
    for (int i = 1; i <= n; i++) {
        if (back[i] <= 0) continue;
        back[i]--;
        if (topsort()) {
            cout << "YES\n";
            return 0;
        }
        back[i]++;
    }
    cout << "NO\n";
    return 0;
}

P4816 [USACO15DEC] High Card Low Card G

思路

贪心题,对于 \(a\) 数组的前一半,要战胜 \(a_i\),那么就要选择一个 \(b_j > a_i\)。那么我们先将 \(a\) 的前一半从大到小排序,然后丢出 Bessie 有的最大值 \(\max\),如果 \(\max < a_i\),Bessie 就输了就继续试探 \(a_{i - 1}\),否则 Bessie 赢了。

对于 \(a\) 的后面,直接将上面的做法反过来即可。

代码

/*******************************
| Author:  SunnyYuan
| Problem: P4816 [USACO15DEC] High Card Low Card G
| OJ: Luogu
| URL:     https://www.luogu.com.cn/problem/P4816
| When:    2023-10-13 11:09:37
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 1000010;

int n, a[N], s[N];
bool st[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    for (int i = 1; i <= n; i++) cin >> a[i], s[a[i]] = 1;
    sort(a + 1, a + 1 + n / 2, greater<int>());
    sort(a + 1 + n / 2, a + n + 1);

    int ans = 0;
    int l = 1, r = n << 1;
    for (int i = 1; i <= (n >> 1); i++) {
        while (r >= 1 && s[r]) r--;
        if (r < a[i]) continue;
        ans++;
        r--;
    }
    for (int i = 1; i <= (n >> 1); i++) {
        while ((l <= (n << 1)) && s[l]) l++;
        if (l > a[i + (n >> 1)]) continue;
        ans++;
        l++;
    }
    cout << ans << '\n';
    return 0;
}

P3203 [HNOI2010] 弹飞绵羊

思路

对于每一个元素 \(x\) 处理它跳出当前块到达的位置 \(p_x\) 与步数 \(step_x\)

怎么更新位置 \(x\) 呢?

如果 \(x + a_x\) 还在同一块内,那么 \(p_x = p_{x + a_x}, step_x = step_{x + a_x} + 1\)

如果 \(x + a_x\) 不在同一块内,那么 \(p_x = x + a_x, step_x = 1\)

对于询问,我们不断让 \(ans\) 加上 \(step_x\),然后 \(x\) 跳到 \(p_x\) 继续加,直到 \(x > n\) 时停止。

代码

/*******************************
| Author:  SunnyYuan
| Problem: P3203 [HNOI2010] 弹飞绵羊
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P3203
| When:    2023-10-14 20:04:37
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 200010;

int n, c, a[N];
int p[N], s[N];
int L[N], R[N];
int sy[N];
int m;

int query(int x) {
    int ans = 0;
    while (x <= n) {
        ans += s[x];
        x = p[x];
    }
    return ans;
}

void modify(int x, int v) {
    a[x] = v;
    int j = sy[x];
    for (int i = R[j]; i >= L[j]; i--) {
        int nxt = i + a[i];
        if (nxt > R[j]) p[i] = nxt, s[i] = 1;
        else p[i] = p[nxt], s[i] = s[nxt] + 1;
        sy[i] = j;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    c = sqrt(n);
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= c; i++) {
        L[i] = R[i - 1] + 1;
        R[i] = L[i] + c - 1;
    }
    if (R[c] != n) {
        c++;
        L[c] = R[c - 1] + 1;
        R[c] = n; 
    }
    for (int j = c; j >= 1; j--) {
        for (int i = R[j]; i >= L[j]; i--) {
            int nxt = i + a[i];
            if (nxt > R[j]) p[i] = nxt, s[i] = 1;
            else p[i] = p[nxt], s[i] = s[nxt] + 1;
            sy[i] = j;
        }
    }
    cin >> m;

    for (int i = 1, opt, a, b; i <= m; i++) {
        cin >> opt;
        if (opt == 1) {
            cin >> a;
            a++;
            cout << query(a) << '\n';
        }
        else {
            cin >> a >> b;
            a++;
            modify(a, b);
        }
    }
    return 0;
}

HDU-5057 Argestes and Sequence

思路

将每个数字拆成很多位并分别进行统计。

非常适合分块题。

单点修改:\(O(1)\)

区间询问:\(O(\sqrt n)\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: Argestes and Sequence
| OJ:      Virtual Judge - HDU
| URL:     https://vjudge.net/problem/HDU-5057#author=634579757
| When:    2023-10-14 20:34:27
| 
| Memory:  32 MB
| Time:    2500 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

const int N = 100010, V = 510;

int n, m, c;
int a[N];
int L[V], R[V], p[N];
int sum[V][16][10];
i64 p10[17];

int query(int l, int r, int x, int v) {
    int p1 = p[l], p2 = p[r];
    if (p1 == p2) {
        int cnt = 0;
        for (int i = l; i <= r; i++) {
            cnt += ((a[i] / p10[x - 1]) % 10) == v;
        }
        return cnt;
    }
    else {
        int cnt = 0;
        for (int i = l; i <= R[p1]; i++) {
            cnt += ((a[i] / p10[x - 1]) % 10) == v;
        }
        for (int i = L[p2]; i <= r; i++) {
            cnt += ((a[i] / p10[x - 1]) % 10) == v;
        }
        for (int i = p1 + 1; i < p2; i++) {
            cnt += sum[i][x][v];
        }
        return cnt;
    }
}

void modify(int u, int x) {
    for (int i = 1; i <= 15; i++) {
        sum[p[u]][i][a[u] % 10]--;
        a[u] /= 10;
    }
    a[u] = x;
    for (int i = 1; i <= 15; i++) {
        sum[p[u]][i][x % 10]++;
        x /= 10;
    }
}

void solve() {
    memset(sum, 0, sizeof(sum));
    memset(L, 0, sizeof(L));
    memset(R, 0, sizeof(R));
    cin >> n >> m;
    c = sqrt(n);
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= c; i++) {
        L[i] = R[i - 1] + 1;
        R[i] = L[i] + c - 1;
    }
    if (R[c] != n) {
        c++;
        L[c] = R[c - 1] + 1;
        R[c] = n;
    }
    for (int j = 1; j <= c; j++) {
        for (int i = L[j]; i <= R[j]; i++) {
            int x = a[i];
            for (int k = 1; k <= 15; k++) {
                sum[j][k][x % 10]++;
                x /= 10;
            }
            p[i] = j;
        }
    }

    char opt;
    int a, b, c, d;

    for (int i = 1; i <= m; i++) {
        cin >> opt;
        if (opt == 'Q') {
            cin >> a >> b >> c >> d;
            cout << query(a, b, c, d) << '\n';
        }
        else {
            cin >> a >> b;
            modify(a, b);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    p10[0] = 1;
    for (int i = 1; i < 16; i++) p10[i] = p10[i - 1] * 10;

    int T;
    cin >> T;
    while (T--) solve();

    return 0;
}

CF1886D. Monocarp and the Set

建议标签:数学。

建议难度:普及/提高-。

思路

这个题目非常有意思。

当您苦思冥想都想不出来怎么正着做的时候,不妨把问题反过来想想。

题中说是加数,我们就可以把所有的操作反过来,从 \(n\) 个数字中不断删除数字。

首先如果是 \(>\),那么就只能删除最大值,有一种选法。

其次如果是 \(<\),那么只能删除最小值,也只有一种选法。

当操作为 \(?\) 时,那么既不删除最大值,也不删除最小值,有 \(len - 2\) 种选法,\(len\) 是数组长度。

因为当第 \(i\) 次操作完的时候有 \(i\) 个数字,去除最大最小值,答案就是所有满足 \(s_i = ?\)\((i - 2)\) 的乘积。

对于每一个询问,如果要将是问号的一位改成别的(\(s_i = '?' \rightarrow s_i = >/<\)),那么就要除以 \((i - 2)\),这个可以使用逆元实现,不会的可以去补一补数论。当然,如果是一个别的字符改成问号也要记得乘上 \((i - 2)\)

还要特判,如果在只有两个数字的时候有 \(?\),那不可能完成!因为除了最大值就是最小值,所以要直接输出 \(0\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: D. Monocarp and the Set
| Contest: Codeforces - Educational Codeforces Round 156 (Rated for Div. 2)
| URL:     https://codeforces.com/contest/1886/problem/D
| When:    2023-10-11 14:41:49
| 
| Memory:  256 MB
| Time:    2000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 300010, mod = 998244353;

int n, m, ans;
int modfs[N];
char s[N];

int pow(int a, int b) {
    int res = 1;
    while (b) {
        if (b & 1) res = 1ll * res * a % mod;
        a = 1ll * a * a % mod;
        b >>= 1;
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> (s + 2);
    for (int i = 1; i <= n; i++) modfs[i] = pow(i, mod - 2);
    ans = 1;
    for (int i = 3; i <= n; i++)
        if (s[i] == '?')
            ans = 1ll * ans * (i - 2) % mod;

    if (s[2] == '?') cout << "0\n";
    else cout << ans << '\n';

    int p;
    char x;
    while (m--) {
        cin >> p >> x;
        p++;
        if (s[p] == '?' && p > 2) ans = 1ll * ans * modfs[p - 2] % mod;
        if (x == '?' && p > 2) ans = 1ll * ans * (p - 2) % mod;
        s[p] = x;
        if (s[2] == '?') cout << "0\n";
        else cout << ans << '\n'; 
    } 
    return 0;
}

AcWing 878. 线性同余方程

题目描述

思路

转化一下:

\[\begin{aligned} ax&\equiv b\pmod {m}\\ ax &= mp + b\\ ax-mp &= b \end{aligned} \]

现在已知 \(a, m\),要求 \(x, p\),可以使用扩展欧几里得算法,不会的可以参考我的博客

求出来的 \(b\) 还必须是 \(\gcd(a, m)\) 的倍数才可以(裴蜀定理),否则无解。

如果有解记得将 \(x\) 乘上 \(\dfrac{b}{\gcd(a, m)}\)

代码

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

int exgcd(int a, int b, int&x, int &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    int g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
}

void solve() {
    int a, b, m;
    cin >> a >> b >> m;
    int x, y;
    int g = exgcd(a, m, x, y);
    if (b % g) cout << "impossible\n";
    else cout << (i64)x * b / g % m << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

AcWing 104. 货仓选址

题目描述

思路

假设我们选点 \(x\),那么题目要求最小化:

\[f(x) = |A_1 - x| + |A_2 - x| + \cdots + |A_N - x| \]

我们对等式进行化简:

假设 \(A_i \le A_{i + 1}\) 对于所有 \(i < n\)

\[\begin{aligned} f(x) &= |A_1 - x| + |A_2 - x| + \cdots + |A_N - x|\\ &=(|A_1 - x| + |A_N - x|) + (|A_2 - x| + |A_{N - 1} - x|) + \cdots\\ &\ge (A_N - A_1) + (A_{N - 1} - A_2) + \cdots \end{aligned} \]

那么到这里大家已经可以看出来了,实际上是取所有数值的中位数就可以让上面的 \(\ge\) 变成 \(=\) 以达到最小化的目的。

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int a[N], n;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    int pos = a[(n + 1) / 2], ans = 0;
    for (int i = 1; i <= n; i++) ans += abs(a[i] - pos);
    cout << ans << '\n';
    return 0;
}

CF979C Kuro and Walking Route

思路

其实本题很简单,只要将树的根换为 \(x\),然后答案就是 \(n \times (n - 1)\) 减去这两个 \(size\) 相乘,即所有的方案减去这两个被我圈出来的部分的点两两组合的个数。

第一个 \(size_1\) 就是原本的 \(size_x\) 减去 \(y\) 所在的子树的 \(size_v\)

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 300010, M = 600010;

struct edge {
    int to, next;
} e[M];

int head[N], idx;

void add(int u, int v) {
    idx++, e[idx].to = v, e[idx].next = head[u], head[u] = idx;
}

int n, x, y;
int sz[N];

bool dfs(int u, int fa) {
    bool flag = (u == y);
    sz[u] = 1;
    for (int i = head[u]; i; i = e[i].next) {
        int to = e[i].to;
        if (to == fa) continue;
        bool d = dfs(to, u);
        if (!d) sz[u] += sz[to];
        flag |= d;
    }
    return flag;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >>n >> x >> y;

    for (int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }

    dfs(x, 0);
    cout << 1ll * n * (n - 1) - 1ll * sz[x] * sz[y] << '\n';
    return 0;
}

P9745 「KDOI-06-S」树上异或

前言

借鉴了 xyzfrozen 题解的思路,想在这里详细讲一讲。

还有大家好像都不爱画图,看得挺费劲。

LaTeX 公式中的 \(u, v\) 很像,请注意甄别。

思路

首先设 \(f_{i, j, k}\) 在以 \(i\) 为根的子树中,对于每一种割边的方法,点 \(i\) 所在连通块异或出来的值在二进制表示下的第 \(j\) 位为 \(k\) 的情况下,其他连通块的异或的乘积之和。

比如下图的割边方式:

\(f_{i, j, k}\) 就记录着所有割边情况的乘积之和。

介绍好了状态,我们想一想怎么转移,对于每一条边 \((u, v)\),假设 \(u\)\(v\) 的父亲节点。

因为我们要从上一个状态递推到这个状态,所以先将 \(f_{u}\) 拷贝到一个临时数组 \(g\),即 \(g_{i, j} = f_{u, i, j}\),每次枚举新边的时候重新拷贝。

转移要分类讨论:

  1. 首先,我们可以让 \(v\) 所在连通块并入 \(u\) 所在的连通块:

对于将 \(v\) 并入 \(u\) 的部分,并且 \(v\) 所在连通块的异或和为 \(l\)\(u\) 所在连通块的异或和为 \(k\)

\[f_{u, j, k\oplus l} = \sum g_{j, k}\cdot f_{v, j, l} \]

  1. 然后,我们也可以让 \(v\) 还是一个独立的连通块,不要并入 \(u\)

我们可以计算出 \(v\) 的贡献:\(t = \sum\limits_{i = 0}^{63}2^if_{v,i,1}\)

然后:

\[f_{u, j, k} = \sum g_{j, k}\cdot t \]

我认为我讲的比较详细了,如果还不懂,建议先做一做P2015 二叉苹果树

代码

非常详细的注释,大家可以慢慢看。

/*******************************
| Author:  SunnyYuan
| Problem: P9745 「KDOI-06-S」树上异或
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P9745
| When:    2023-10-20 12:10:24
| 
| Memory:  512 MB
| Time:    2000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

const int N = 500010, mod = 998244353;

vector<int> e[N];           // 建图
i64 n, x[N];                // 存储信息
int f[N][64][2];            // DP
int pow2[64];               // pow2[i] 保存 2 的 i 次幂

void dfs(int u, int fa) {
    int tmp[64][2];         // 保存当前状态
    for (int to : e[u]) {   // 遍历每一条边
        if (to == fa) continue;// 是父节点,不退回去
        dfs(to, u);         // 走到子节点
        memcpy(tmp, f[u], sizeof(tmp));// 保存当前状态
        memset(f[u], 0, sizeof(f[u]));// 清空重新计算

        int ans_v = 0;      // 计算 to 所在子树可以给的贡献

        for (int j = 0; j < 64; j++) {// 对于每一位计算贡献(ans_v)
            (ans_v += 1ll * f[to][j][1] * pow2[j] % mod) %= mod;
        }

        for (int j = 0; j < 64; j++) {// 现在正在计算第 j 位的贡献
            for (int k = 0; k < 2; k++) {// 枚举 u 的第 j 位是 0 还是 1
                (f[u][j][k] += 1ll * ans_v * tmp[j][k] % mod) %= mod;// u 不让 to 所在连通块并进来
                for (int x = 0; x < 2; x++) {
                    (f[u][j][k ^ x] += 1ll * tmp[j][k] * f[to][j][x] % mod) %= mod;// u 让 to 所在连通块并进来,所以我们要枚举 to 的第 j 位是 0 还是 1
                }
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    pow2[0] = 1;            // 2 的 0 次幂是 1
    for (int i = 1; i < 64; i++) (pow2[i] = pow2[i - 1] + pow2[i - 1]) %= mod; // 初始化 2 的 i 次幂

    cin >> n;               // 点数
    for (int i = 1; i <= n; i++) cin >> x[i];// 输入点权
    for (int i = 2; i <= n; i++) {// 输入每一条边
        int to;
        cin >> to;
        e[to].push_back(i); // 建立双向边
        e[i].push_back(to);
    }

    for (int i = 1; i <= n; i++)// 第 i 个数字
        for (int j = 0; j < 64; j++)// 第 j 位
            f[i][j][x[i] >> j & 1] = 1;// 初始化 x[i]

    dfs(1, 0);              // 树形 dp

    i64 ans = 0;            // 答案
    for (int j = 0; j < 64; j++) {// 统计答案
        (ans += 1ll * f[1][j][1] * pow2[j] % mod) %= mod;
    }
    cout << ans << '\n';    // 输出答案
    return 0;
}

P1441 砝码称重

思路

这道题目是 dfs + dp 的练手好题。

首先我们可以使用 dfs 从 \(n\) 中挑选 \(m\) 个。

然后做一个类似 01 背包的东西统计个数。

\(f_i\) 表示若干个砝码堆出重量 \(i\) 的方案数。

对于每一个选择的物体 \(i\)

\[f_{j} = \sum f_{j - a_i} \]

代码

/*******************************
| Author:  SunnyYuan
| Problem: P1441 砝码称重
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/P1441
| When:    2023-10-03 23:45:38
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 25, M = 2010;

int n, m, a[N], f[M];
int sum;
bool choose[N];
int cnt;
bool c[M];
int ans;

void dfs(int u) {
    if (cnt > m) return;
    if (u > n) {
        if (cnt < m) return;
        memset(f, 0, sizeof(f));
        memset(c, 0, sizeof(c));
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            if (choose[i]) continue;
            for (int j = M - 1; j >= a[i]; j--) f[j] += f[j - a[i]];
        }
        for (int j = 1; j < M; j++) {
            if (f[j]) c[j] = true;
        }
        sum = 0;
        for (int j = 1; j < M; j++) sum += c[j];
        ans = max(ans, sum);
        return;
    }
    cnt++;
    choose[u] = true;
    dfs(u + 1);
    cnt--;
    choose[u] = false;
    dfs(u + 1);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    dfs(1);
    cout << ans << '\n';
    return 0;
}

CF1407D Discrete Centrifugal Jumps

思路

首先,不难想到,是这两种情况才可以转移:

然后我们可以维护两个单调栈:

对于这两种情况,我们要仔细想一想怎么从之前的一个 \(i\) 转移到 \(j\) 了,注意是\(i\) 转移到 \(j\)

首先,我们想一想,现在我们来到了 \(j\)

看下图:是不是栈中任何一个满足 \(h_k < h_j\),都可以将 \(k\) 到栈顶的所有元素作为下凸的那一个部分,然后 \(k\) 前面的一个作为 \(i = k - 1\) 可以转移到 \(j\)

\[f_{j} = \min(f_{j}, f_{i} + 1) \]

实际上,我们想让 \(j\) 离得越远越好,所以可以边弹出边转移,直到 \(a_k > a_i\) 的时候停止。

第二种上凸的情况就不说了,就把上面的东西全部反过来就可以了。

代码

/*******************************
| Author:  SunnyYuan
| Problem: Discrete Centrifugal Jumps
| Contest1: Luogu
| URL:     https://www.luogu.com.cn/problem/CF1407D
| When:    2023-10-05 19:39:46
| 
| Memory:  250 MB
| Time:    2000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 300010;

int n, h[N], f[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> h[i];
    stack<int> s1, s2;
    f[0] = -1;
    for (int i = 1; i <= n; i++) {
        f[i] = f[i - 1] + 1;// 跳一步
        while (s1.size() && h[s1.top()] <= h[i]) {	// 第一种情况
            int x = s1.top();
            s1.pop();
            if (s1.size() && min(h[s1.top()], h[i]) > h[x]) f[i] = min(f[i], f[s1.top()] + 1);
        }
        while (s2.size() && h[s2.top()] >= h[i]) {	// 第二种情况
            int x = s2.top();
            s2.pop();
            if (s2.size() && max(h[s2.top()], h[i]) < h[x]) f[i] = min(f[i], f[s2.top()] + 1);
        }
        s1.push(i);
        s2.push(i);
    }
    cout << f[n] << '\n';
    return 0;
}

CF1614D1 Divan and Kostomuksha (easy version)

前言

基本思路,zltqwq 大佬已经讲的很清楚了,我想补充一下怎么转移 DP。

long long 卡了 6 发。

思路

先说说基本思路。

首先我们计算出 \(c_i\) 表示所有数字中有因数 \(i\) 的数字个数,设 \(f_i\) 表示以 \(i\)公因数时可以贡献的和(不包括不能整除 \(i\) 的)。

最开始时 \(f_i = c_i \cdot i\)

转移方程为 \(f_{i} = f_{i\cdot p} + i\cdot (c_i - c_{i \cdot p})\)

这表示啥呢?这个实际上是先将 \(c\cdot p\) 的倍数放在前面,然后将 \(c_{i} - c_{i\cdot p}\) 个数字放在后面,因为 \(i\) 的倍数中包含 \(i\cdot p\) 的倍数,所以要减掉,这样保证了前面的 \(\gcd\)\(c\cdot p\),后面的是 \(\gcd\)\(c\),目的就是让和最大化。

最后的答案就是 \(ans = \max\limits_{c_i = n} f_i\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: Divan and Kostomuksha (easy version)
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF1614D1
| When:    2023-10-20 20:25:20
| 
| Memory:  1000 MB
| Time:    4000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

const int N = 100010, M = 5000010;

int prime[M], cnt;
bool inp[M];

void getprime() {
    inp[1] = inp[0] = 1;

    for (int i = 2; i < M; i++) {
        if (!inp[i]) prime[++cnt] = i;
        for (int j = 1; j <= cnt && prime[j] * i < M; j++) {
            inp[prime[j] * i ] = true;
            if (i % prime[j] == 0) break;
        }
    }
    
}

int n, a[N];
i64 f[M];
int c[M];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    getprime();

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= a[i] / j; j++) {
            if (a[i] % j == 0) {
                c[j]++;
                if (a[i] / j != j) c[a[i] / j]++;
            }
        }
    }
    for (int i = 0; i < M; i++) f[i] = 1ll * c[i] * i;

    for (int i = M - 1; i >= 1; i--) {
        for (int j = 1; j <= cnt && 1ll * i * prime[j] < M; j++) {
            f[i] = max(f[i], f[i * prime[j]] + 1ll * i * (c[i] - c[i * prime[j]]));
        }
    }
    i64 ans = 0;
    for (int i = 0; i < M; i++)
        if (c[i] == n)
            ans = max(ans, f[i]);
    cout << ans << '\n';
    return 0;
}

CF915E Physical Education Lessons

思路

这道题目看起来非常 ODT,但是显然我们可以用动态开点的方法来解决这个题目。

维护 l, r 表示左右儿子。

维护 tag 表示是否会修改区间。

维护 sum 表示该区间不是工作日的和,最后的答案就是 n - sum

代码

然后今天很晚了,懒得再写 pushup, pushdown, addtag 了,直接全部写在一个函数里面了。

然后空间能开多大开多大,然后就卡过了,然后就没有然后了。

#include <bits/stdc++.h>

using namespace std;

const int SIZE = 15000000;

struct node {
    int l, r;
    int tag, sum;
} tr[SIZE];

int idx = 1, root = 1;
int n, q;

void modify(int u, int l, int r, int pl, int pr, int k) {
    if (pl <= l && r <= pr) {
        if (k == 1) tr[u].tag = 1, tr[u].sum = r - l + 1;
        else tr[u].tag = 2, tr[u].sum = 0;
        return;
    }


    int mid = l + r >> 1;
    if (tr[u].tag) {
        if (!tr[u].l) tr[u].l = ++idx;
        if (!tr[u].r) tr[u].r = ++idx;
        tr[tr[u].l].tag = tr[u].tag;
        tr[tr[u].r].tag = tr[u].tag;

        if (tr[u].tag == 1) tr[tr[u].l].sum = mid - l + 1, tr[tr[u].l].tag = 1;
        else tr[tr[u].l].sum = 0, tr[tr[u].l].tag = 2;

        if (tr[u].tag == 1) tr[tr[u].r].sum = r - mid, tr[tr[u].r].tag = 1;
        else tr[tr[u].r].sum = 0, tr[tr[u].r].tag = 2;
        
        tr[u].tag = 0;
    }

    if (pl <= mid) {
        if (!tr[u].l) tr[u].l = ++idx;
        modify(tr[u].l, l, mid, pl, pr, k);
    }
    if (pr > mid) {
        if (!tr[u].r) tr[u].r = ++idx;
        modify(tr[u].r, mid + 1, r, pl, pr, k);
    }

    tr[u].sum = tr[tr[u].l].sum + tr[tr[u].r].sum;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1, l, r, k; i <= q; i++) {
        cin >> l >> r >> k;
        modify(root, 1, n, l, r, k);
        cout << n - tr[1].sum << '\n';
    }
    return 0;
}

P1052 [NOIP2005 提高组] 过河

思路

\(f_i\) 表示到 \(i\) 需要踩到的最小石头数量,\(r_i\) 表示第 \(i\) 个位置有没有石头,有:

\[f_i = \min(f_i, f_{i - j} + r_i) \]

但是这个会 TLE,因为 \(L\)\(10^9\),所以如果相邻两个石子之间的距离大于 100,我们就可以把它变成 100,因为有对于互质的两个数字 \(p, q\), 有 \(x \ge 0, y\ge 0\) 使得 \(px + qy > (p - 1)(q - 1) - 1\),不会的可以看看 P3951。我们取 \(p = 10, q = 9\) 就会发现每次走 \(10\)\(9\) 部就可以到达所有 \(72\) 及以上的长度,然后再考虑左右边界可以取 \(100\)。对于长度大于 \(100\) 的,直接将它的距离改称 \(100\) 即可。

同时注意要特判 \(S = T\) 的情况,直接统计 \(r_i \bmod S = 0\) 的情况数量。

代码

/*******************************
| Author:  SunnyYuan
| Problem: P1052 [NOIP2005 提高组] 过河
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P1052
| When:    2023-10-25 10:57:04
| 
| Memory:  128 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 110, M = 300000;

int l, s, t, m, a[N], f[M], g[M];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> l >> s >> t >> m;
    int K = 1;
    for (int i = s; i <= t; i++) K *= i;
    for (int i = 1; i <= m; i++) cin >> a[i];
    a[++m] = l;
    sort(a + 1, a + m + 1);
    for (int i = m; i; i--) {
        a[i] -= a[i - 1];
        int k = a[i] % K;
        if (a[i] >= K && k <= t) k += K;
        a[i] = k;
    }
    for (int i = 1; i <= m; i++) a[i] += a[i - 1];
    for (int i = 1; i < m; i++)  g[a[i]] = 1;
    memset(f, 0x3f, sizeof(f));
    f[0] = 0;
    for (int i = 0; i < M; i++)
        for (int j = s; j <= t; j++)
            if (i >= j) f[i] = min(f[i], f[i - j] + g[i]);
    int ans = 0x3f3f3f3f;
    for (int i = a[m]; i < a[m] + t; i++) ans = min(ans, f[i]);
    cout << ans << '\n';
    return 0;
}

P3243 [HNOI2015] 菜肴制作

思路

首先我们可以想到出度\(0\) 的点不会对其他点的顺序产生影响,所以我们就要在所有出度为 \(0\) 的点中选取较小的放在前面,所以这也是大多数题解为什么要建反图并且取最大的字典序的原因。

代码

建反图 + 拓扑排序 + 贪心。

/*******************************
| Author:  SunnyYuan
| Problem: P3243 [HNOI2015] 菜肴制作
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P3243
| When:    2023-10-27 19:24:39
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using PII = pair<int, int>;

const int N = 100010;

vector<int> e[N];
int in[N];
int n, m;

void solve() {
    for (int i = 1; i <= n; i++) e[i].clear(), in[i] = 0;

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        e[v].push_back(u);
        in[u]++;
    }

    priority_queue<int, vector<int>, less<int> > q;

    for (int i = 1; i <= n; i++) if (!in[i]) q.push(i);

    vector<int> ans;

    while (q.size()) {
        int t = q.top();
        ans.push_back(t);
        q.pop();

        for (int to : e[t]) {
            if (!(--in[to])) {
                q.push(to);
            }
        }
    }

    reverse(ans.begin(), ans.end());

    if (ans.size() == n) for (int x : ans) cout << x << ' ';
    else cout << "Impossible!";
    cout << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) solve();

    return 0;
}

CF939E Maximize!

借鉴了第一篇题解的思路。

思路

贪心,首先当我们每次执行操作 \(1\) 加入新的数字 \(x\) 的时候一定要取,然后后我们发现还可以从第一个数字开始往后取直到平均值变小。

代码

/*******************************
| Author:  SunnyYuan
| Problem: Maximize!
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF939E
| When:    2023-10-28 09:05:18
| 
| Memory:  250 MB
| Time:    3000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

const int N = 500010;

i64 sum[N], a[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    int opt, x, cnt = 0, l = 0;

    cout << setprecision(10) << fixed;

    while (T--) {
        cin >> opt;
        if (opt == 1) {
            cin >> x;
            cnt++;
            a[cnt] = x;
            sum[cnt] = sum[cnt - 1] + x;
        }
        else {
            double res = sum[l] + a[cnt];
            while (res / (l + 1) > (res + a[l + 1]) / (l + 2)) {
                res += a[l + 1];
                l++;
            }
            cout << a[cnt] - res / (l + 1) << '\n';
        }
    }
    return 0;
}

P2865 [USACO06NOV] Roadblocks G

思路

这是一道经典的求次短路的题目,首先假设 \(dis_{u, 0}\) 表示从起点到 \(u\) 的最短路,\(dis_{u, 1}\) 表示从起点到 \(u\) 的次短路。

假设有边 \((u, v)\),边权为 \(w\),如果 \(dis_{v, 0} > dis_{u, 0} + w\),那么先让 \(dis_{v, 1} \leftarrow dis_{v, 0}\),然后 \(dis_{v, 0} \leftarrow dis_{u, 0} + w\)

然后,我们还可以只更新次短路而不更新最短路,这有分为两种情况:

  1. 直接由 \(u\) 的次短路走到 \(v\),但是比 \(dis_{v, 0}\) 大,即如果 \(dis_{u, 1} + w > dis_{v, 0}\)\(dis_{u, 1} + w < dis_{v, 1}\),那么 \(dis_{v, 1} \leftarrow dis_{u, 1} + w\)
  2. 直接由 \(u\) 的最短路走到 \(v\),但是比 \(dis_{v, 0}\) 大,即 \(dis_{u, 0} + w > dis_{v, 0}\)\(dis_{u, 0} + w < dis_{v, 1}\),那么 \(dis_{v, 1} \leftarrow dis_{u, 0} + w\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: P2865 [USACO06NOV] Roadblocks G
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P2865#submit
| When:    2023-10-28 15:54:10
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 5010, M = 200010;

struct edge {
    int to, next, w;
} e[M];

int head[N], idx = 1;

void add(int u, int v, int w) {
    idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
}

int n, m;
int dis[N][2];
bool vis[N][2];

struct node {
    int dis, type, id;
    bool operator>(const node b) const { return dis < b.dis; }
};

void dijkstra() {
    priority_queue<node, vector<node>, greater<node> > q;
    memset(dis, 0x3f, sizeof(dis));
    dis[1][0] = 0;
    q.push({0, 0, 1});

    while (q.size()) {
        int t = q.top().id, ty = q.top().type;
        q.pop();
        if (vis[t][ty]) continue;
        vis[t][ty] = true;

        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (dis[to][0] > dis[t][0] + e[i].w) {
                dis[to][1] = dis[to][0];
                dis[to][0] = dis[t][0] + e[i].w;
                q.push({dis[to][0], 0, to});
                q.push({dis[to][1], 1, to});
            }
            if (dis[to][0] < dis[t][0] + e[i].w && dis[to][1] > dis[t][0] + e[i].w) {
                dis[to][1] = dis[t][0] + e[i].w;
                q.push({dis[to][1], 1, to});
            }
            if (dis[to][0] < dis[t][1] + e[i].w && dis[to][1] > dis[t][1] + e[i].w) {
                dis[to][1] = dis[t][1] + e[i].w;
                q.push({dis[to][1], 1, to});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1, u, v, w; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w), add(v, u, w);
    }
    dijkstra();
    cout << dis[n][1] << '\n';
    return 0;
}

CF1891C. Smilo and Monsters

建议标签:贪心

思路

贪心题,首先我们可以想到可以使用较小的怪物群组合出 \(x\) 然后用 \(1\) 的代价干掉当前最大的怪物群,直到所有较小的怪物加起来都不能干掉最大的怪物时,才要特判。

当所有较小的怪物加起来都不能干掉最大的怪物时,如果当前已经积累了 \(x\) 个怪物,现在只剩下一个最大的怪物群,有 \(t\) 只怪物,那么我们不妨将他们两个加起来考虑。

\(t + x\) 如果是偶数,那么先消灭 \(s = \left\lfloor\dfrac{t + x}{2}\right\rfloor\),然后再用 \(1\) 的代价消灭 \(s\) 只怪物,如果 \(s\) 是奇数,那么还会多 \(1\),所以答案再加 \(1\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: C. Smilo and Monsters
| OJ:      Codeforces - Codeforces Round 907 (Div. 2)
| URL:     https://codeforces.com/contest/1891/problem/C
| When:    2023-11-01 10:03:36
| 
| Memory:  256 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto& x : a) cin >> x;
    sort(a.begin(), a.end());
    int l = 0, r = n - 1, sum = 0;
    i64 ans = 0;
    while (l <= r) {
        while (l < r && sum < a[r]) {
            int add = min(a[r] - sum, a[l]);
            sum += add;
            a[l] -= add;
            if (!a[l]) l++;
        }
        if (sum == a[r]) {
            ans += sum + 1;
            a[r] = sum = 0;
            r--;
        }
        if (l == r) {
            // cout << l << ' ' << r << ' ' << a[l] << endl;
            int p = sum + a[l];
            if (p & 1) ans += (p / 2) + 1 + (bool)(p / 2);
            else ans += (p / 2) + 1;
            break;
        }
    }
    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) solve();

    return 0;
}

P5098 [USACO04OPEN] Cave Cows 3

我想拓展一下这道题目,而不仅限于是个二维的点,首先说一下基本思路。

思路

题目要求最大化 \(|x_i - x_j| + |y_i - y_j|\)

分类讨论,如果 \(x_i > x_j\)\(y_i > y_j\) 那么答案就是 \(x_i + y_i - (x_j + y_j)\),那么我们就可以最大化 \(x_i + y_i\),最小化 \(x_j + y_j\),如果 \(x_i > x_j\)\(y_i < y_j\),那么答案就是 \(x_i - y_i - (x_j - y_j)\),那么我们就要最大化 \(x_i - y_i\) 和最小化 \(x_j - y_j\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: P5098 [USACO04OPEN] Cave Cows 3
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P5098
| When:    2023-11-01 11:29:16
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    int max1 = -1e9, max2 = -1e9, min1 = 1e9, min2 = 1e9;

    for (int i = 1; i <= n; i++) {
        int x, y;
        cin >> x >> y;
        max1 = max(max1, x + y);
        max2 = max(max2, x - y);
        min1 = min(min1, x + y);
        min2 = min(min2, x - y);
    }

    cout << max(max1 - min1, max2 - min2) << '\n';
    return 0;
}

拓展

我们可以将 2 维拓展到 \(k\) 维。

同样的思路,对于点 \((x_1, x_2, x_3, x_4, x_5, \cdots x_k)\),我们可以先将第一维固定是 \(+\),即 \(x_1\),然后分类讨论后面的维度,它其实是可加可减的,比如,对于 \((a_1, a_2, a_3)\)\((b_1, b_2, b_3)\) 这两个三维的点,先固定 \(a_1 > b_1\),假设 \(a_2 < b_2, a_3 > b_3\),那么就会有

\[\begin{aligned} &|a_1 - b_1| + |a_2 - b_2| + |a_3 - b_3|\\ &= a_1 - b_1 + b_2 - a_2 + a_3 - b_3\\ &= (a_1 - a_2 + a_3) - (b_1 - b_2 + b_3) \end{aligned} \]

那么我们就要在所有 \(x_1 - x_2 + x_3\) 中选取最大值和最小值并相减。

那么对于每一维 \(a_j\)\(b_j\) 都有两种关系:大于或小于,所以我们对于 \(k\) 维的点,我们可以使用暴搜枚举两两之间的关系,再对每一种关系取 \(\max\),这样可以将复杂度从 \(O(n^2)\) 变为 \(O(2^kn)\),如果 \(k\) 很小这个优化是有效的,比如 \(k \le 4\) 可以写出:

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

const i64 INF = 1e16;

struct maxmin {
    i64 maxx, minn;
    maxmin() { maxx = -INF, minn = INF; }
    void update(i64 x) { maxx = max(maxx, x), minn = min(minn, x); }
    i64 res() { return maxx - minn; }
} q[8];

int n, k, t[5], cnt;

void dfs(int u, i64 sum) {
    if (u > k) {
        q[cnt++].update(sum);
        return;
    }
    dfs(u + 1, sum + t[u]);
    dfs(u + 1, sum - t[u]);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= k; j++) cin >> t[j];
        cnt = 0;
        dfs(2, t[1]);
    }
    i64 res = 0;
    for (int i = 0; i < 8; i++) res = max(res, q[i].res());
    cout << res << '\n';
    return 0;
}

CF958F2 Lightsabers (medium)

思路

设总共有 \(n\) 个数字,值域为 \(m\)\(c_x\) 表示数字 \(x\) 在某一段的个数,\(a\) 表示原数组,\(b_x\) 表示要数字 \(x\) 要达到的个数目标。

首先我们先圈出一段使得所有数字 \(c_x \ge b_x\),那么对于圈出来的这一段我们可以使用 \(\sum\limits_{i = 1}^{m} c_i - b_i\) 的次数删除多余的数字。

我们可以使用双指针完成上述过程,设当前 \((l, r)\) 已经使 \(cnt\) 个数字 \(c_i \ge b_i\),要删除 \(ans\) 个数据才能达到目标,那么固定一个左端点 \(l\),然后只要还没有凑够(\(cnt < m\)),就让 \(r\)\(1\),并且让 \(c_{a_r}\)\(1\),如果 \(c_{a_r} = b_{a_r}\) 就让 \(cnt\)\(1\),如果 \(c_{a_r} > b_{a_r}\),那么说明又多来了一个根本不应该来的家伙,要把它删去,即 \(ans \leftarrow ans + 1\),最后在所有 \(ans\) 中取 \(\min\)

代码

/*******************************
| Author:  SunnyYuan
| Problem: Lightsabers (medium)
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF958F2
| When:    2023-11-01 15:20:39
| 
| Memory:  250 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 200010;

int n, m;
int a[N], b[N], c[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i], c[a[i]]++;
    int cnt = 0;
    for (int i = 1; i <= m; i++) cin >> b[i], cnt += (b[i] == 0);
    memset(c, 0, sizeof(c));
    int ans = 0x3f3f3f3f, sum = 0;
    for (int l = 1, r = 0; l <= n; l++) {
        while (r < n && cnt < m) {
            c[a[++r]]++;
            if (c[a[r]] == b[a[r]]) cnt++;
            if (c[a[r]] > b[a[r]]) sum++;
        }
        if (cnt == m) ans = min(ans, sum);
        else break;
        if (c[a[l]] > b[a[l]]) sum--;
        c[a[l]]--;
        if (c[a[l]] < b[a[l]]) cnt--;
    }
    if (ans == 0x3f3f3f3f) cout << "-1\n";
    else cout << ans << '\n';
    return 0;
}

P6902 [ICPC2014 WF] Surveillance

思路

首先,它是一个环,不好处理,我们可以断环为链,所有区域也都复制一遍,我们设 \(f_{i, j}\) 为从 \(i\) 开始走 \(2^j\) 个区域可以到达的最大值,那么有 \(f_{i, j} = f_{f_{i, j - 1} + 1, j - 1}\),即我们可以先从 \(i\)\(2^{j - 1}\) 步到 \(f_{i, j - 1}\),然后再从 \(f_{i, j - 1} + 1\)\(2^{j - 1}\) 个区域跳到最大值。

然后,我们对于每一个 \(i\) 试探其要跳到 \(i + n - 1\) 需要的最小步骤 \(step\),我们可以像倍增求 \(LCA\) 一样不断试探,从大到小枚举 \(j\),如果当前位置 \(cur\) 再往前走 \(2^j\) 个区域没有跨过 \(i + n - 1\),即 \(f_{cur + 1, j} < i + n - 1\),那么让 \(cur \leftarrow f_{cur + 1, j}\),试探完成后再让 \(step \leftarrow step + 1\),跨过 \(i + n - 1\)

最后,我们要谈一谈怎么初始化,这个问题虽然比较小,但是也还是拿出来说一说,就是我们先将所有的区域按左端点排序,接着我们用 \(i\)\(1\) 枚举到 \(n\),然后所有区域中左端点 \(l \le i\) 的区域右端点最大值可以作为 \(f_{i, 0}\),表示其走一步可以到达的最大点,因为枚举的 \(i\) 和区域的左端点都是单调不减的,所以双指针可以在 \(O(n)\) 内完成。

总时间复杂度:\(O(n \log n)\)

代码

注意:

  1. 在程序中,为了提速,我将 \(f_{i, j}\) 换成了 \(f_{j, i}\)
  2. 如果有一个区域 \(l > r\),那么将 \(r \leftarrow r + n\)
/*******************************
| Author:  SunnyYuan
| Problem: P6902 [ICPC2014 WF] Surveillance
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P6902
| When:    2023-11-01 14:14:05
| 
| Memory:  1 GB
| Time:    4000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;
using PII = pair<int, int>;

const int N = 2000010, INF = 0x3f3f3f3f, K = 22;

int f[K][N];
int n, m;
PII a[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;

    int cnt = m;
    for (int i = 1; i <= m; i++) {
        cin >> a[i].first >> a[i].second;
        if (a[i].first > a[i].second) a[i].second += n;
        else a[++cnt] = {a[i].first + n, a[i].second + n};
    }
    sort(a + 1, a + cnt + 1);

    int l = 0, maxx = 0;
    for (int i = 1; i <= (n << 1); i++) {
        while (l + 1 <= cnt && a[l + 1].first <= i) maxx = max(maxx, a[++l].second);
        f[0][i] = maxx;
    }

    for (int j = 1; j < K; j++)
        for (int i = 1; i <= (n << 1); i++)
            if (f[j - 1][i] + 1 <= (n << 1) && f[j - 1][i])
                f[j][i] = f[j - 1][f[j - 1][i] + 1];

    int ans = INF;
    for (int i = 1; i <= n; i++) {
        int k = i - 1, step = 0;
        for (int j = K - 1; j >= 0; j--) {
            if (f[j][k + 1] < i + n - 1 && f[j][k + 1]) {
                k = f[j][k + 1];
                step |= (1 << j);
            }
        }
        if (f[0][k + 1] >= i + n - 1) step++;
        else step = INF;
        ans = min(ans, step);
    }
    if (ans == INF) cout << "impossible\n";
    else cout << ans << '\n';
    return 0;
}

P2519 [HAOI2011] problem a

非常有意思的一道紫题。

思路

首先我们发现一个人有 \(x\) 个比他大的和 \(y\) 个比他小的人,那么其名次一定在 \(x + 1 \sim n - y\) 之间。

因此可以计算出他们的名次的范围 \([l, r]\),如果 \(l > r\) 就一定是矛盾的,直接略过。

再者,如果有 \(cnt\) 个相同的区间 \([l, r]\),那么只能取 \(v = \min(cnt, r - l + 1)\) 个说真话的人,我们可以将每个区域可以获得的最大说真话的人数 \(v\) 记作每个区间的权值。

如果 \([l_1, r_1]\)\([l_2, r_2]\) 有交集,那么两个区间只能取一个,因为 \([l, r]\) 表示的是相同成绩的一个区间,所以我们只要求出不相交集合的权值之和的最大值就可以了。

不相交集合的权值之和的最大值可以使用动态规划完成:

\(f_i\) 表示从第一个集合到第 \(i\) 个集合可以获得的最大权值。

\[f_i = \max(f_{i - 1}, f_k + v_i) \]

\(k\) 表示在 \(1\sim i - 1\) 中找出最大的 \(k\) 满足 \(r_k < l_i\),这个可以用排序 + 二分实现。

最后的答案是 \(n - f_{tot}\)\(tot\) 是集合个数。

代码

代码中有 n, cnt, tot 来表示不同的意义的总量,要注意区分。

/*******************************
| Author:  SunnyYuan
| Problem: P2519 [HAOI2011] problem a
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P2519
| When:    2023-11-02 15:40:06
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

struct node {
    int l, r, v;
} a[N];

int n;
int f[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    int cnt = 0;
    node tmp;
    for (int i = 1, x, y; i <= n; i++) {
        cin >> x >> y;
        tmp = {x + 1, n - y, 0};
        if (tmp.l > tmp.r) continue;
        a[++cnt] = tmp;
    }
    sort(a + 1, a + cnt + 1, [](const node a, const node b) { return (a.l == b.l) ? a.r < b.r : a.l < b.l; });
    int tot = 0;
    for (int i = 1; i <= cnt; i++) {
        if (a[i].l == a[tot].l && a[i].r == a[tot].r) a[tot].v++;
        else a[++tot] = a[i], a[tot].v = 1;
    }
    for (int i = 1; i <= tot; i++) a[i].v = min(a[i].v, a[i].r - a[i].l + 1);
    sort(a + 1, a + tot + 1, [](const node a, const node b) { return (a.r == b.r) ? a.l < b.l : a.r < b.r; });
    // for (int i = 1; i <= tot; i++) cout << a[i].l << ' ' << a[i].r << ' ' << a[i].v << endl;
    for (int i = 1; i <= tot; i++) {

        int l = 0, r = tot;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (a[mid].r < a[i].l) l = mid;
            else r = mid - 1;
        }
        // cout << i << ' ' << l << endl;
        f[i] = max(f[i - 1], f[l] + a[i].v);
    }
    cout << n - f[tot] << '\n';
    return 0;
}

UVA529 Addition Chains

题解

首先这道题目不能直接暴搜,因为(可能)会 T,然后我们可以大概得出需要 \(\log n\) 次,所以我们发现这道题目的答案较小,直接 dfs / bfs 都会保存大量的中间无效状态,所以 IDDFS 板子题,直接开写。

等等!我们可以进行优化,第一,假设现在的 \(x\cdot 2^{(dep - cur)} < n\),就不用继续往下搜;第二,可以标记一个 vis 数组,不要搜相同的和(基本没用);第三,可以先搜大的,后搜小的,应该会更快。

代码

/*******************************
| Author:  SunnyYuan
| Problem: Addition Chains
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/UVA529
| When:    2023-11-03 19:37:06
| 
| Memory:  0 MB
| Time:    3000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int n, dep;
int ans[N];

bool dfs(int u) {
    if (u == dep && ans[u] == n) return true;
    if (ans[u] * (1 << (dep - u)) < n) return false;// 优化 1
    for (int i = u; i >= 1; i--) {					// 优化 2
        for (int j = i; j >= 1; j--) {
            int sum = ans[i] + ans[j];
            if (sum > ans[u] && sum <= n) {
                ans[u + 1] = sum;
                if(dfs(u + 1)) return true;
            }
        }
    }
    return false;
}

void solve() {
    for (dep = 1; ; dep++) {
        ans[1] = 1;
        if (dfs(1)) {
            for (int j = 1; j <= dep; j++) cout << ans[j] << " \n"[j == dep];
            break;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    while (cin >> n, n) solve();
    return 0;
}

CF1895D. XOR Construction

这个题目赛场上交了 2 发,在赛后又交了 5 发才过,欢迎爆踩。

思路

看了一眼题解区,为啥都用了 Trie,(直接)贪心不香吗?

首先,我们都不难想到:

\[\begin{aligned} b_1 \oplus b_2 &= a_1\\ b_2 \oplus b_3 &= a_2\\ \cdots\\ b_j \oplus b_{j + 1} &= a_j\\ \end{aligned} \]

把左右两边都异或起来可以推出:\(b_1 \oplus b_{j + 1} = \bigoplus\limits_{i = 1}^{j}a_i\)

记前缀异或和 \(sum_x = \bigoplus\limits_{i = 1}^{x}a_i\)

那么 \(b_{j + 1} = b_1 \oplus sum_{j}\),那么实际上是只要确定了 \(b_1\) 就可以算出所有的 \(b_j\)

题目中要求要让 \(b\)\(0\sim n - 1\) 之内,这实际上实在寻找一个 \(b_1\) 使得异或出来的所有值越小越好,所以我们拆位,假设所有数字的第 \(i\) 位为 \(1\) 的个数大于为 \(0\) 的个数,那我们最好异或上一个 \(2^i\),这样可以使大部分数字变小。

代码

/*******************************
| Author:  SunnyYuan
| Problem: D. XOR Construction
| OJ:      Codeforces - Educational Codeforces Round 157 (Rated for Div. 2)
| URL:     https://codeforces.com/contest/1895/problem/D
| When:    2023-11-04 09:06:50
| 
| Memory:  512 MB
| Time:    2000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 1; i < n; i++) {
        cin >> a[i];
        if (i) a[i] ^= a[i - 1];
    }
    a[0] = 0;
    int ans = 0;
    for (int i = 0; i < 31; i++) {
        int sum1 = 0, sum2 = 0;
        for (int j = 0; j < n; j++) {
            if (a[j] >> i & 1) sum1++;
            else sum2++;
        }
        if (sum1 > sum2) ans |= 1 << i;
    }
    for (int i = 0; i < n; i++) a[i] ^= ans;
    for (int i = 0; i < n; i++) cout << a[i] << ' ';
    return 0;
}


为什么一定可以到 \(0\sim n - 1\),问的人太多了,我截取了我回答的照片:

CF1895E Infinite Card Game

思路

首先我们初始化出每个人出牌后对手出的牌,因为我们要保证消灭对手的同时最大化自己的防御能力,所以第一件事情就是使用排序 + 二分 + 后缀最大值求出对于每一张牌 \((x, y)\) 的对方出的 \((t, v)\) 使得 \(t > x\) 且让 \(v\) 最大化,找出后我们将它们连边,即 \((x, y) \rightarrow (t, v)\)

然后我们进行拓扑排序,如果拓扑排序后还有剩余的点,那么这些点会形成环,这些点就会造成平手。

然后对于剩下的部分,我们设 \(f_i\) 表示第一次就出牌 \(i\) 谁会赢,如果 \(f_i = 1\) 那么表示先手 Monocarp 赢,\(f_i = 2\) 后手 Bicarp 赢,\(f_i = 0\) 表示此点处于一个环中,上面已经讨论过。

我们从后往前遍历拓扑序,对于当前点 \(u\),如果存在边 \((u, v)\),就将 \(v\) 的状态继承过来,即 \(f_u = f_v\),如果 \(u\) 的出度为 \(0\),那么判断 \(u\)\(n\) 的关系,如果 \(u \le n\),那么 \(f_u = 1\),否则 \(f_u = 2\)

最后遍历整个 \(f\) 数组求助 3 个答案。

代码

注意不能用 memset 初始化,会 TLE。

/*******************************
| Author:  SunnyYuan
| Problem: Infinite Card Game
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF1895E
| When:    2023-11-04 15:13:20
| 
| Memory:  500 MB
| Time:    3000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 600010, INF = 0x3f3f3f3f;

struct edge {
    int to, next;
} e[N];

int head[N], in[N], idx;

void add(int u, int v) {
    idx++, e[idx].to = v, e[idx].next = head[u], head[u] = idx, in[v]++;
}

struct node {
    int x, y;
    bool operator<(const node b) const { return x < b.x; }
} p1[N], p2[N];

int n, m;
int max1[N], max2[N];
int f[N];

void solve() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> p1[i].x;
    for (int i = 1; i <= n; i++) cin >> p1[i].y;
    cin >> m;
    for (int i = 1; i <= m; i++) cin >> p2[i].x;
    for (int i = 1; i <= m; i++) cin >> p2[i].y;

    // 初始化
    for (int i = 0; i <= n + m; i++) f[i] = in[i] = 0, head[i] = 0;
    max1[n + 1] = max2[m + 1] = idx = 0;

    sort(p1 + 1, p1 + n + 1);
    sort(p2 + 1, p2 + m + 1);

    for (int i = n; i >= 1; i--) {
        max1[i] = max1[i + 1];
        if (p1[max1[i]].y < p1[i].y) max1[i] = i;
    }
    for (int i = m; i >= 1; i--) {
        max2[i] = max2[i + 1];
        if (p2[max2[i]].y < p2[i].y) max2[i] = i;
    }

    for (int i = 1; i <= n; i++) {
        int l = 1, r = m + 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (p2[mid].x > p1[i].y) r = mid;
            else l = mid + 1;
        }
        if (l > m) continue;
        add(i, max2[l] + n);
    }

    for (int i = 1; i <= m; i++) {
        int l = 1, r = n + 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (p1[mid].x > p2[i].y) r = mid;
            else l = mid + 1;
        }
        if (l > n) continue;
        add(i + n, max1[l]);
    }

    vector<int> ans;
    queue<int> q;

    for (int i = 1; i <= n + m; i++) if (!in[i]) q.push(i);
    while (q.size()) {
        int t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (!(--in[to])) q.push(to);
        }
    }
    int ans1 = 0, ans2 = 0, ans3 = 0;
    for (int j = ans.size() - 1; j >= 0; j--) {
        int cur = ans[j];
        for (int i = head[cur]; i; i = e[i].next) {
            int to = e[i].to;
            f[cur] = f[to];
        }
        if (!head[cur]) {
            if (cur > n) f[cur] = 2;
            else f[cur] = 1;
        }
    }
    for (int i = 1; i <= n; i++) { // 注意这里是 n 而不是 n + m
        if (f[i] == 1) ans1++;
        if (f[i] == 0) ans2++;
        if (f[i] == 2) ans3++;
    }
    cout << ans1 << ' ' << ans2 << ' ' << ans3 << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) solve();

    return 0;
}

P2151 [SDOI2009] HH去散步

好久沒有記錄生活了,所以,開記!

思路

開始的時候看錯題了,沒有看到不能走回頭路,所以直接把點連接在了一起,然後矩陣快速冪,然後竟然可以拿到 10 分。

但是如果只考慮點,不好連邊,因爲你不好保證不會走回頭路,所以我們點邊互換!

我們將所有的邊變成點,如果相鄰的兩條邊不是相同的且不是反邊,我們就將這兩條邊的編號連接起來,走 \(t\) 步就相當於在邊上走 \(t - 1\) 步,所以最後的矩陣快速冪要將 \(t - 1\)

最後 \(O(n^2)\) 枚舉出來兩條邊(現在已經變成點了)\(i, j\),且邊 \(i\) 起點是 \(u\), 邊 \(j\) 的重點是 \(t\),將這個答案 \(g_{i, j}\) 加到答案中。

代碼

注意對反邊的處理,如果使用異或處理,一定要從 \(0\) 或者 \(2\) 開始。

/*******************************
| Author:  SunnyYuan
| Problem: P2151 [SDOI2009] HH去散步
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/P2151
| When:    2023-11-08 10:41:57
| 
| Memory:  125 MB
| Time:    1000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

const int N = 55, M = 130, mod = 45989;

struct road {
    int u, v;
} e[M];

struct graph {
    graph() { memset(a, 0, sizeof(a)); } 
    int a[M][M];
} g;

int n, m, t, a, b;

graph operator*(graph a, graph b) {
    graph res;
    for (int i = 0; i < (m << 1); i++) {
        for (int j = 0; j < (m << 1); j++) {
            for (int k = 0; k < (m << 1); k++) {
                (res.a[i][j] += a.a[i][k] * b.a[k][j]) %= mod;
            }
        }
    }
    return res;
}

graph pow(graph a, int b) {
    graph res;
    for (int i = 0; i < (m << 1); i++) res.a[i][i] = 1;
    while (b) {
        if (b & 1) res = res * a;
        a = a * a;
        b >>= 1;
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> t >> a >> b;
    a++, b++;
    for (int i = 0, u, v; i < (m << 1); i += 2) {
        cin >> u >> v;
        u++, v++;
        e[i] = {u, v}, e[i + 1] = {v, u};
    }

    for (int i = 0; i < (m << 1); i++) {
        for (int j = 0; j < (m << 1); j++) {
            if (i != j && i != (j ^ 1) && e[i].v == e[j].u) {
                g.a[i][j] = 1;
            }
        }
    }

    g = pow(g, t - 1);

    int ans = 0;
    for (int i = 0; i < (m << 1); i++) {
        for (int j = 0; j < (m << 1); j++) {
            if (e[i].u == a && e[j].v == b) {
                (ans += g.a[i][j]) %= mod;
            }
        }
    }
    cout << ans << '\n';
    return 0;
}

CF978F Mentors

思路

这道题目非常简单,首先我们使用二分,每次求出所有 \(a\) 数组中比 \(a_i\) 小的数字个数,记为 \(ans_i\),然后我们处理口角,我们只要贪心,每次将能力较大的那个人的 \(ans - 1\) 即可,因为我们总不能因为一个学员不听话就不欢迎其他学生。

代码

/*******************************
| Author:  SunnyYuan
| Problem: Mentors
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/CF978F
| When:    2023-11-08 11:29:08
| 
| Memory:  250 MB
| Time:    3000 ms
*******************************/

#include <bits/stdc++.h>

using namespace std;

int n, k;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;

    vector<int> ans(n, 0), b(n), a(n);
    for (int i = 0; i < n; i++) cin >> a[i], b[i] = a[i];
    sort(a.begin(), a.end());
    for (int i = 0; i < n; i++) {
        int l = -1, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (a[mid] < b[i]) l = mid;
            else r = mid - 1;
        }
        ans[i] = l + 1;
        // cout << ans[i] << ' ';
    }
    cout << endl;
    while (k--) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        if (b[u] < b[v]) ans[v]--;
        else if (b[v] < b[u]) ans[u]--;
    }
    for (auto x : ans) cout << x << ' ';

    return 0;
}

AcWing 3167 / UVA10228 A Star not a Tree

思路

本题可以使用三分套三分,但是,模拟退火似乎更简单。

很板,不多说了,不会的可以参考我的博客

代码

注意:

  1. acwing 上只有一个测试数据;
  2. UVA 除了最后一个测试点都要输出两个换行,最后一个测试点要一个换行,否则 WA。
/*******************************
| Author:  SunnyYuan
| Problem: A Star not a Tree?
| OJ:      Luogu
| URL:     https://www.luogu.com.cn/problem/UVA10228
| When:    2023-11-08 16:08:40
| 
| Time:    3000 ms
*******************************/

#include <bits/stdc++.h>

#define x first
#define y second

using namespace std;
using PDD = pair<double, double>;

const int N = 110;

int n;
PDD p[N];
double ans;

double dis(const PDD a, const PDD b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

double proc(const PDD s) {
    double res = 0;
    for (int i = 1; i <= n; i++) res += dis(p[i], s);
    ans = min(ans, res);
    return res;
}

double rnd(double l, double r) {
    return l + 1.0 * (r - l) * rand() / (double)RAND_MAX;
}

void simu() {
    PDD u(rnd(0, 10000), rnd(0, 10000));
    for (double t = 10000; t > 1e-4; t *= 0.97) {
        PDD v(rnd(u.x - t, u.x + t), rnd(u.y - t, u.y + t));
        double delta = proc(v) - proc(u);
        if (exp(-delta / t) > rnd(0, 1)) u = v;
    }
}

void solve(bool endline) {
    ans = 1e100;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> p[i].x >> p[i].y;
    for (int i = 1; i <= 1000; i++) simu();
    cout << ans << '\n';
    if (endline) cout << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout << setprecision(0) << fixed;

    int T;
    cin >> T;
    for (int i = 1; i <= T; i++) solve(i != T);

    return 0;
}

P2704 炮兵阵地

非常经典的一道题目。

思路

\(f_{i, j, k}\) 表示第 \(i\) 行的状态为 \(j\),第 \(i - 2\) 行的状态为 \(k\),那么有:

\[f_{i, j, k} = \max\{f_{i - 1, k, l} + cnt(j)\} \]

\(cnt(x)\) 表示数字 \(x\) 在二进制表示下的 \(1\) 的个数。

同时我们还要判断是否满足题目条件。

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 100, M = 1024;

int n, m;
int g[N];
int f[N][M][M];

int getcnt(int x) {
	int cnt = 0;
	for (int j = 31; j >= 0; j--) if (x >> j & 1) cnt++;
	return cnt;
}

bool check(int x) {
	return ((x & (x << 1)) == 0) && ((x & (x << 2)) == 0);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			char c;
			cin >> c;
			if (c == 'P') g[i] |= (1 << j);
		}
	}
	
	int ans = 0;
	for (int j = 0; j < (1 << m); j++) {
		if (check(j) && ((j & g[0]) == j)) ans = max(ans, f[0][j][0] = getcnt(j));
	}
	
	for (int i = 1; i < n; i++) {
		for (int j = 0; j < (1 << m); j++) {
			if (!check(j)) continue;
			for (int k = 0; k < (1 << m); k++) {
				if (!check(k)) continue;
				for (int p = 0; p < (1 << m); p++) {
					if ((!check(p)) || (j & k) || (j & p) || ((j & g[i]) != j)) continue;
					ans = max(ans, f[i][j][k] = max(f[i][j][k], f[i - 1][k][p] + getcnt(j)));
				}				
			}
		}
	}
	cout << ans << '\n';
	return 0;
}

P2167 [SDOI2009] Bill的挑战

很有意思的一道题目。

思路

状态:\(dp_{i, j}\) 表示从左往右前 \(i\) 位选取的字符串为二进制下的 \(j\) 时的合法方案数。

答案:\(\sum dp_{len(s), j}\)

辅助状态:\(f_{i, j}\) 表示下标为 \(i\) 的字符为 \(j\) 的字符串的集合。

状态转移方程:\(dp_{i + 1, j \text{ and } f_{i, c}} = \sum dp_{i, j}\)\(\text{and}\) 表示逻辑与。

初始状态:\(dp_{0, 0} = 1\)

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 16, M = 55, K = 1 << 15, C = 128, mod = 1000003;

char s[N][M];
int f[M][C], dp[M][K];
int n, k, len;

int getcnt(int x) {
	int cnt = 0;
	for (int i = 31; i >= 0; i--) {
		if (x >> i & 1) cnt++;
	}
	return cnt;
}

void solve() {
	memset(dp, 0, sizeof(dp));
	memset(f, 0, sizeof(f));
	
	cin >> n >> k;
	for (int i = 0; i < n; i++) cin >> s[i];
	len = strlen(s[0]);
	
	for (int i = 0; i < len; i++) {
		for (char j = 'a'; j <= 'z'; j++) {
			for (int k = 0; k < n; k++) {
				if (s[k][i] == j || s[k][i] == '?') f[i][j] |= (1 << k);
			}
		}
	}
	
	for (char c = 'a'; c <= 'z'; c++) dp[0][f[0][c]]++;
	
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < (1 << n); j++) {
			for (char c = 'a'; c <= 'z'; c++) {
				(dp[i + 1][j & f[i + 1][c]] += dp[i][j]) %= mod;
			}
		}
	}
	
	int ans = 0;
	for (int i = 0; i < (1 << n); i++) {
		if (getcnt(i) == k) {
			(ans += dp[len - 1][i]) %= mod;
		}
	}
	cout << ans << '\n';
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	int T;
	cin >> T;
	while (T--) solve();
	
	return 0;
} 

P3694 邦邦的大合唱站队

思路

\(f_i\) 为已经排完的人的集合在二进制表示下为 \(i\),那么有

\[f_i = \min\{f_{i - 2^j} + R - L + 1 - sum_{[l, r], j}\} \]

\(L, R\) 是可以推算出来的。

代码

本来以为从 \(0\) 开始肯定好写,结果细节比较多(?)还是这样写了。

#include <bits/stdc++.h>

using namespace std;

const int N = 20, M = 100010;

int f[1 << N];
int n, m, a[M];
int sum[M][N];

int gettot(int x) {
	int tot = -1;
	for (int j = 0; j < m; j++) {
		if (x >> j & 1) tot += sum[n - 1][j];
	}
	return tot;
}

int getsum(int l, int r, int x) {
	if (l) return r - l + 1 - (sum[r][x] - sum[l - 1][x]);
	else return r - l + 1 - (sum[r][x]);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		if (i) memcpy(sum[i], sum[i - 1], sizeof(sum[i]));
		cin >> a[i];
		a[i]--;
		sum[i][a[i]]++;
	}
	
	memset(f, 0x3f, sizeof(f));
	f[0] = 0;
	for (int i = 1; i < (1 << m); i++) {
		for (int j = 0; j < m; j++) {
			if (i >> j & 1) {
				int l = gettot(i - (1 << j)) + 1;
				int r = l + sum[n - 1][j] - 1;
				f[i] = min(f[i], f[i - (1 << j)] + getsum(l, r, j));
			} 
		}
	}
	cout << f[(1 << m) - 1] << '\n';
	return 0;
}

P1516 青蛙的约会

思路

实际上是要解 \((x + m \cdot cnt) \bmod L = (y + n \cdot cnt) \bmod L\)

转化:

\[\begin{aligned} x + m\cdot cnt&\equiv y + n \cdot cnt\pmod L\\ (m - n)\cdot cnt + L \cdot k &= y - x \end{aligned} \]

\(x - y, m - n\) 已知,可求 \(cnt, k\)

\(m - n\) 是个负数,将 \(x, y\)\(m, n\) 分别互换。

代码

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

i64 x, y, m, n, l;

i64 exgcd(i64 a, i64 b, i64 &x, i64 &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    i64 g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	
	cin >> x >> y >> m >> n >> l;
	if (m < n) swap(m, n), swap(x, y);
	// int a = m - n , b = L, c = y - x;
	i64 ans_a = 0, ans_b = 0;
	i64 g = exgcd(m - n, l, ans_a, ans_b);
	
	if ((y - x) % g) {
		cout << "Impossible\n";
		return 0;
	}
	ans_a *= (y - x) / g;
	((ans_a %= l / g) += l / g) %= l / g;
	cout << ans_a << '\n';
	return 0;
} 

posted @ 2024-07-20 16:14  SunnyYuan  阅读(15)  评论(0编辑  收藏  举报