2024.2.26

前言

还有 \(4\) 天就结束了呜呜呜,我还不想走,我还没打过国赛呜呜呜。

博弈

以为是个吊题,结果真是签到题啊QAQ。

首先我们要读明白题,我们一个点可以放多个棋子,所以可以得出一个结论:每个点是互不影响的。

所以我们可以每个点分开来算。

正如题解所说:“因为在自己所属点上的棋子是完全由自己操控的,不会莫名其妙地被对方抢走”,所以最优策略就是让自己可以走的步数减去对面可以走的步数。

然后我们就可以很轻易的得到一个 \(dp_i\),表示若 \(i\) 有棋子,\(A\) 可以走的比 \(B\) 多的步数。若 \(i\) 为白色,那么 \(dp_i\) 为最大值;若 \(i\) 为黑色,那么 \(dp_i\) 为最小值。

注意要把每个 \(dp_i\) 赋为 \(0\),因为如果这个点实在不符合当前出手的人的利益,那么就不管这个点了。同时也可以将棋子全在黑点的情况抛去。

最后的统计答案的时候背包,找大于 \(0\) 的方案就行。

博客园

#include <bits/stdc++.h>

const int SIZE = 310;
const int mod = 998244353;

int N, M;
int cnt = 1, head[SIZE], to[SIZE * SIZE], next[SIZE * SIZE];
int degree[SIZE], dp[SIZE], f[2 * SIZE * SIZE];

char str[SIZE];
bool color[SIZE];

void AddEdge(int u, int v) {
    ++ cnt;
    next[cnt] = head[u];
    head[u] = cnt;
    to[cnt] = v;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);

    freopen("game.in", "r", stdin);
    freopen("game.out", "w", stdout);

    std::cin >> N >> M;
    std::cin >> (str + 1);
    for (int i = 1; i <= N; ++ i)
        color[i] = (str[i] == 'W' ? 0 : 1);
    for (int i = 1, x, y; i <= M; ++ i) {
        std::cin >> x >> y;
        if (x < y)
            std::swap(x, y);
        degree[y] ++;
        AddEdge(x, y);
    }
    for (int now = N; now >= 1; -- now) {
        for (int i = head[now]; i; i = next[i]) {
            if (color[to[i]] == 0) 
                dp[to[i]] = std::max(dp[to[i]], dp[now] + 1);
            if (color[to[i]] == 1) 
                dp[to[i]] = std::min(dp[to[i]], dp[now] - 1);
        }
    }

    int begin = 301 * 301;
    f[begin] = 1;
    for (int i = 1; i <= N; ++ i) {
        if (dp[i] < 0) {
            for (int j = begin - 300 * 300; j <= begin + 300 * 300; ++ j) {
                if (f[j] == 0)
                    continue;
                f[j + dp[i]] = (f[j + dp[i]] + f[j]) % mod;
            }
        }
        else {
            for (int j = begin + 300 * 300; j >= begin - 300 * 300; -- j) {
                if (f[j] == 0)
                    continue;
                f[j + dp[i]] = (f[j + dp[i]] + f[j]) % mod;
            }
        }
    }
    int answer = 0;
    for (int i = begin + 1; i <= begin + 300 * 300; ++ i)
        answer = (answer + f[i]) % mod;
    std::cout << answer << '\n';
    return 0;
}

排列

呃呃呃,这道题属实有些逆天了。

依靠人类智慧,我们发现对于题目的限制,我们可以将点对 \((a,b)\) 连边。

然后我们称一个区间是好(或区间是有传递性的)的,当且仅当在一个区间内,若 \(a\) 能到达 \(c\),则 \(a\)\(c\) 一定有连边。

这样我们对于一个题目要求的合法排列一定存在:排列任意的前缀和任意的后缀都是好的。

然鹅上面和正解关系不大

根据爆搜,我们可以知道对于一个 \(DAG\)(有向无环图),自身和补图(若自身是前缀,则补图是除了自身以外的后缀)同时是好的的个数是 \(n!\) 的。可以爆搜!

那我们怎么维护呐?

继续人类智慧QAQ:

我们先拿出来的个 \(1\)\(n\) 的严格单调递增的排列。对于相邻的 \(a,b\) 并且 \(a < b\),我们若交换 \(a,b\) 则表示 \(a, b\) 连边。最后我们求变成排列 \(n\)\(1\) 的方案数就行。

对于题目里要求的情况,我们每次交换 \(c,d\) 的时候判断一下 \(a,b\) 有没有交换过就行。

洛谷
// ubsan: undefined
// accoders
#include <bits/stdc++.h>

typedef long long ll;

const int mod = 998244353;

int N, M;
std::vector<std::pair<int, int>> limit[11][11];
std::vector<int> edge[4000000];
int array[11], pos[11], small[11];
ll fac[11], dp[4000000];

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);

    freopen("perm.in", "r", stdin);
    freopen("perm.out", "w", stdout);

    std::cin >> N >> M;
    for (int i = 1, a, b, c, d; i <= M; ++i) {
        std::cin >> a >> b >> c >> d;
        limit[c][d].emplace_back(a, b);
    }

    fac[0] = 1;
    for (int i = 1; i <= N; ++i) {
        fac[i] = fac[i - 1] * i;
        array[i] = i;
    }

    int id = 0;
    do {
        ++id;
        for (int i = 1; i <= N; ++i) {
            pos[array[i]] = i;
            small[i] = 0;
            for (int j = i + 1; j <= N; ++j)
                if (array[i] > array[j])
                    ++small[i];
        }

        for (int i = 2; i <= N; ++i) {
            if (array[i - 1] > array[i])
                continue;

            int c = array[i - 1];
            int d = array[i];
            bool flag = true;

            for (const std::pair<int, int> &iter : limit[c][d]) {
                if (pos[iter.first] < pos[iter.second]) {
                    flag = false;
                    break;
                }
            }

            if (!flag)
                continue;

            int to = id;

            to -= fac[N - i] * small[i];
            to -= fac[N - i + 1] * small[i - 1];
            to += fac[N - i] * small[i - 1];
            to += fac[N - i + 1] * (small[i] + 1);

            // std::cout << id << ' ' << to << ' ' << i - 1 << ' ' << i << '\n';
            // for (int j = 1; j <= N; ++ j)
            //     std::cout << small[j] << ' ';
            // std::cout << '\n';

            edge[id].emplace_back(to);
        }

    } while (std::next_permutation(array + 1, array + 1 + N));

    dp[1] = 1;
    for (int i = 1; i <= id; ++i)
        for (const int &to : edge[i]) dp[to] = (dp[to] + dp[i]) % mod;
    std::cout << dp[id] << '\n';

    return 0;
}

子段和

先想一想暴力。

我们发现:若我们现在求删完了 \(k\) 的值不能从 \(k-1\) 转移过来。那么我们只能每次求一遍 \(k\) 能到的最小值。

然后我就不会了。

但是题解告诉我:我们二分答案最后的最小值就行了。

我的内心:哈哈_,你真幽默。

具体说说做法。

暴力写法1

我们每次二分答案可以到达的最小值。

在二分的 \(check\) 里面,我们维护一个前缀和的最小值,然后用现在的前缀和减去前缀和最小值,看一下是不是大于二分的值,若大于则让现在这个点的值减去大于的部分就行了。

注意我们对于每个点减去的值,在这个点之后的前缀和里面都要减去。

最后判断减去的值打不大于我们的 \(k\) 就行了。

但是我的写法好像没办法优化。

csdn
#include <bits/stdc++.h>

typedef long long ll;

const int SIZE = 1100;
const int mod = 998244353;

int N, K;
ll a[SIZE], pre[SIZE], answer;

bool check(int max, int rest) {
    max -= 2e7;
    ll min = 0, differ = 0;
    for (int i = 1; i <= N; ++ i) {
        ll now = pre[i] - differ;
        if (now - min > max)
            differ += (now - min - max);
        now = pre[i] - differ;
        min = std::min(min, now);
    }
    return (differ <= rest);
}

ll Answer(int k) {
    int l = 1, r = 4e7 + 1, result = 0;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (check(mid, k)) {
            result = mid;
            r = mid - 1;
        }
        else {
            l = mid + 1;
        }
    }
    return result - 2e7;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);

    std::cin >> N;
    for (int i = 1; i <= N; ++ i) {
        std::cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    std::cin >> K;
    for (int i = 1; i <= K; ++ i) {
        answer += Answer(i);
        answer %= mod;
    }
    std::cout << answer << '\n';
    return 0;
}

暴力写法2

同样是二分,只不过改一下 \(check\)

我们先设 \(limit\) 表示我们前缀和可以到的最大值。

然后若当前位置的前缀和大于 \(limit\),那么我们就减去大于的部分就行。

但是这里把减法变成加法了,我们将此时的 \(limit\) 赋值成 \(pre_i\)(表示 \(1\)\(i\) 的前缀和) 就行。

若我们之后一直保持现在的 \(limit\),那么意味着后半部分一直受修改之前的影响。

\(limit\) 被更新成 \(pre_i+max\)\(max\) 表示二分的区间最大和为 \(max\)) 了,那么就不受修改之前的影响了。

codeforces
#include <bits/stdc++.h>

typedef long long ll;

const int SIZE = 1100;
const int mod = 998244353;

int N, K;
ll a[SIZE], pre[SIZE], answer;

bool check(int max, int rest) {
    max -= 2e7;
    ll limit = max, has = 0;
    for (int i = 1; i <= N; ++ i) {
        if (pre[i] > limit) {
            has += pre[i] - limit;
            limit = pre[i];
        }
        limit = std::min(limit, pre[i] + max);
    }
    return (has <= rest);
}

ll Answer(int k) {
    int l = 1, r = 4e7 + 1, result = 0;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (check(mid, k)) {
            result = mid;
            r = mid - 1;
        }
        else {
            l = mid + 1;
        }
    }
    return result - 2e7;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0);
    std::cout.tie(0);

    std::cin >> N;
    for (int i = 1; i <= N; ++ i) {
        std::cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    std::cin >> K;
    for (int i = 1; i <= K; ++ i) {
        answer += Answer(i);
        answer %= mod;
    }
    std::cout << answer << '\n';
    return 0;
}
posted @ 2024-02-26 22:02  觉清风  阅读(37)  评论(0编辑  收藏  举报