概述

不简单,但也不算特别难。

Div1 的 BCDE 都值得一做。

因为写得比较着急,可能有各种疏漏,见谅。

Div2 A

Description

对于一个长度为 $n$ 的排列 $p$,定义其 fingerprint $F(p)$ 为 $p$ 中相邻元素和排序后得到的数组。更形式化地,

$$F(p)=\text{sort}([p_1+p_2,p_2+p_3,\cdots,p_{n-1}+p_n])$$​。

给定一个长度为 $n$ 的排列 $p$,你需要找到一个与 $p$​ 不同但 fingerprint 相同的排列。

Solution

没啥可说的,看一看这个定义,因为是排列,直接倒序输出即可。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

int T;

int main()
{
    cin >> T;
    while (T--)
    {
        int n;
        cin >> n;
        std::vector<int> a;
        for (int i = 1, x; i <= n; i++)
            cin >> x, a.push_back(x);
        std::reverse(a.begin(), a.end());
        for (int u : a)
            cout << u << " ";
        cout << endl;
    }
}

Div2 B

Description

给定一个序列 $a[1]-a[n]$​,满足 $\sum_{i=1}^{n}a[i]=0$​。

每次可以选两个数 $i$​,$j$​,使 $a[i]$ ​变为 $a[i]-1$​, $a[j]$ ​变为 $a[j]+1$​。

当 $i<j$ ​​使操作无花费,否则操作花费 $1$​​。

求使所有数都变为 $0$ ​所需的最小花费。

Solution

显然我们需要尽可能进行无花费的操作。

尽可能让前面的数减小,后面的数增加,但是要是已经是负的了,那么就需要花费代价。

想一想再玩一玩样例,发现答案是最小前缀和的相反数(感性理解)。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
#include <cmath>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

const int maxn = 1e5 + 10;

int n;
int a[maxn];
long long sum[maxn];

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n;
        long long ans = 1ll << 60;
        for (int i = 1; i <= n; i++)
            cin >> a[i], sum[i] = sum[i - 1] + a[i], ans = std::min(ans, sum[i]);
        cout << std::abs(ans) << endl;
    }
}

A

Description

给定一个二进制字符串 由 0,1,? 组成。

? 可以是 1 或者 0,由你自由选择。

问你是否能构造出一个字符串,使得这个字符串每个长度为 $k$ 的子串中 01 的个数相等。

Solution

不会做就找规律()

看样例发现这个东西很循环,往这个方面想一想就能想到:第 $s_i=s_{i+k}$。很显然不证明了。

那么整个字符串就被分成了 $k$ 组,把已经确定的组都确定好,如果组内有矛盾显然无解。接着只检查 $s_1\sim s_k$​ 合不合法即可。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
#include <cmath>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

const int maxn = 3e5 + 10;

int a[maxn], n, k;
char s[maxn];

bool solve()
{
    cin >> n >> k >> (s + 1);
    for (int i = 1; i <= n; i++)
        a[i] = s[i] == '?' ? -1 : s[i] - '0';
    for (int i = 1; i <= k; i++)
    {
        int p = a[i];
        for (int j = i + k; j <= n; j += k)
            if (a[j] != -1)
                p = a[j];
        for (int j = i; j <= n; j += k)
            if (a[j] != -1 && a[j] != p)
                return false;
        a[i] = p;
    }
    int cnt1 = 0, cnt2 = 0;
    for (int i = 1; i <= k; i++)
        if (a[i] == 1)
            cnt1++;
        else if (a[i] == -1)
            cnt2++;
    if (cnt1 <= k / 2 && cnt1 + cnt2 >= k / 2)
        return true;
    return false;
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        puts(solve() ? "YES" : "NO");
    }
}

B

Description

有一棵 $n$ 个点的树, Alice 和 Bob 初始在这棵树上的节点 $a$, $b$。

他们可以在树上轮流移动一段距离不超过 $da$ 和 $db$ 的路径。

路径长度的定义是两点之间树上简单路径的边数。

如果 Alice 能在 $10^{100}$​ 次内追到 Bob ,那么则算 Alice 赢,否则算 Bob 赢。

Alice 先走。

Solution

看起来是个结论题,实际上也确实是。

想一想加画一画能想到 Alice 获胜有以下三种情况。否则输。

  • $dis(a,b)\leq da$,一步就抓到了;
  • $2da\geq db$,可以一步一步逼近 Bob 胜利;
  • Alice 走到一个能控制树上所有点的点,这样 Bob 不管怎么走都会输。这个时候需要满足 $2da\geq d$,$d$ 是树的直径。

第一种情况很显然,第二种情况也挺显然:不管 Bob 怎么走,Alice 一定能拉近距离。

第三种情况是很极限的情况,Bob 最远也只能走直径长度的路径,如果 Alice 能占据直径中点那自然必胜(建议画图理解)。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

const int maxn = 1e5 + 10;

int n, a, da, b, db;

std::vector<int> e[maxn];
inline void add(int x, int y) { e[x].push_back(y), e[y].push_back(x); }

void clear()
{
    for (int i = 1; i <= n; i++)
        e[i].clear();
}

int dep[maxn];

void dfs(int x, int fa)
{
    dep[x] = dep[fa] + 1;
    for (int v : e[x])
        if (v != fa)
            dfs(v, x);
}

bool solve()
{
    dep[0] = -1, dfs(b, 0);
    int rt = 0;
    int dis = dep[a];
    if (dis <= da || 2 * da >= db)
        return true;
    for (int i = 1; i <= n; i++)
        if (dep[i] > dep[rt])
            rt = i;
    dfs(rt, 0);
    for (int i = 1; i <= n; i++)
        if (dep[i] > dep[rt])
            rt = i;
    int d = dep[rt];
    //cout << d << endl;
    if (da + da >= d)
        return true;
    return false;
}

int main()
{
    std::ios::sync_with_stdio(false);
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n >> a >> b >> da >> db;
        for (int i = 1, x, y; i < n; i++)
            cin >> x >> y, add(x, y);
        puts(solve() ? "Alice" : "Bob");
        clear();
    }
}

C

Description

给定一个含有 $n$​ 个正整数的序列 $a$​ ,对于一次操作,你可以任选一个位置 $i$​ 且满足 $a_i=i$​ ,那么就可以移除这个元素,并将后面所有的元素向前移动一位。

对于每个相互独立的询问 $x,y$ 需要你求出在前 $x$ 个元素以及后 $y$ 个元素不能被移除的情况下,最多可以进行几次操作。

$n,q \leq 3\times 10^5$。

Solution

能想到能删除几个点和删除的顺序是没什么关系的,只要一个数前面能被删除的数足够多那么他就可以被删除(因为先删后面的不影响前面的)。

考虑到这一点,我们直接让 $a_i=i-a_i$,每次删除一个数相当于区间减 $1$ 。显然负数永远都消不掉。

这种奇怪的东西我们尝试把它离线下来。

然后就不会了,我数据结构太差了qwq,直接看题解。。。

要求解答案,我们需要尝试维护 $f_i$ 表示从 $i$ 开始能删掉几个数。

我们把询问按照右端点排序,考虑加进去一个数之后对上面这个东西的贡献。

显然 $f_i$ 是单调不增的,也就是说,对于前一部分,会使 $f_i$ 增加 $1$,后面不变。

或者换个说法:$f_k\geq q_r>f_{k+1}$。

区间加 $1$,单点查询,选择树状数组。

实际上只需要记录操作次数即可。

至于怎么找这个 $k$​,树状数组上二分即可。

有时间会写个线段树版,树状数组上二分还是很难受的。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

const int maxn = 3e5 + 10;

inline int lowbit(int x) { return x & -x; }

int n, a[maxn], t[maxn];

void modify(int x, int v)
{
    for (; x <= n; x += lowbit(x))
        t[x] += v;
}

int query(int x)
{
    int res = 0;
    for (; x; x -= lowbit(x))
        res += t[x];
    return res;
}

int kth(int k)
{
    int res = 0;
    for (int i = 1 << 18; i; i >>= 1)
    {
        if (res + i <= n && k - t[res + i] > 0)
            res += i, k -= t[res];
    }
    return res + 1;
}

struct Q
{
    int l, r, ans, id;
    bool operator<(const Q &b) const { return r < b.r; }
} q[maxn];
bool cmp(const Q &a, const Q &b) { return a.id < b.id; }

int main()
{
    int m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]), a[i] = i - a[i];
    for (int i = 1, l, r; i <= m; i++)
        scanf("%d%d", &l, &r), q[i].id = i, q[i].l = 1 + l, q[i].r = n - r;
    std::sort(q + 1, q + 1 + m);
    int now = 1;
    for (int i = 1; i <= m; i++)
    {
        while (now <= q[i].r)
        {
            int pos = a[now] < 0 ? 1 : std::min(kth(now - a[now]), now + 1);
            modify(pos, 1), now++;
        }
        q[i].ans = q[i].r - query(q[i].l);
    }
    std::sort(q + 1, q + 1 + m, cmp);
    for (int i = 1; i <= m; i++)
        printf("%d\n", q[i].ans);
}

D

Description

这是一道交互题

给定 $2n$ 个数 $1,2\sim 2n$,A 和 B 进行交互,如下规则:

  • A 需要将元素分成 $n$ 组 $\mathbf{pair}$(二元组)
  • B 从每组 $\mathbf{pair}$ 中选择一个元素,如果权值和是 $2n$ 的倍数那么 B 胜,否则 A 胜。

你需要选择 A/B 中的一者扮演角色,并取得胜利。

$n\le 5\times 10^5$​。

Solution

观察样例 + 手算,发现当 $n$ 是偶数的时候,A 可以胜利:排一个 $(1,1+n),(2,2+n)…$ 即可。

简易证明:取出来 $n$ 个数的和在模 $n$ 意义下是 $n(n-1)/2$,设 $n=2m$,则和为 $m(2m-1)$,这玩意是必然没法被 $2m$ 整除的,也就是说在和在模 $n$ 意义下都没法是 $0$,那 $2n$ 就更不行了。

然后盲猜:当 $n$ 是奇数,B 胜利。

有了刚刚的偶数,我们可以考虑试图取在模 $n$ 意义下是 $0,1,2,…,n-1$​ 的数,这些数加在一起一定是 $2n$ 的倍数。

稍微证明一下为什么一定能取到:因为模 $n$ 相同的一个数有两个,取了一个就必然不会取另一个,我们把 $i,i+n$ 连起来,表示只选其中一个。

同一组的两个数也不能同时选,我们也连起来。

这样每个点的度数就是 $2$,也就是说连出了一堆环,且 $i,i+n$​​ 在一个环里且相邻。这样就能得出一定能取到了(好像不太严谨,感性理解吧)。

选一个就不能选另一个,这是啥?无疑是二分图染色。

我们对每个环二分图染色就能得到结果了。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <utility>
#include <vector>
using std::cin;
using std::cout;
using std::endl;
using std::cerr;

void end()
{
    int x;
    cin >> x;
}

const int maxn = 1e6 + 10;
typedef std::pair<int, int> pii;

int used[maxn], must[maxn], vis[maxn];
std::vector<int> e[maxn], ans[2];
inline void add(int x, int y) { e[x].push_back(y), e[y].push_back(x); }
int lst[maxn];

void dfs(int x, int d)
{
    ans[d].push_back(x);
    vis[x] = 1;
    for (int v : e[x])
        if (!vis[v])
            dfs(v, d ^ 1);
}

int main()
{
    std::ios::sync_with_stdio(false);
    int n;
    cin >> n;
    if (n % 2 == 0)
    {
        cout << "First" << endl;
        for (int i = 1; i <= n; i++)
            cout << i << " ";
        for (int i = 1; i <= n; i++)
            cout << i << " ";
        cout << endl;
        end();
        return 0;
    }
    else
    {
        cout << "Second" << endl;
        for (int i = 1; i <= n * 2; i++)
        {
            int x;
            cin >> x;
            if (lst[x])
                add(lst[x], i);
            else
                lst[x] = i;
        }
        for (int i = 1; i <= n; i++)
            add(i, i + n);
        for (int i = 1; i <= 2 * n; i++)
            if (!vis[i])
                dfs(i, 0);
        long long sum = 0;
        for (int k : ans[0])
            sum += k;
        if (sum % (2 * n))
            std::swap(ans[0], ans[1]);
        for (int k : ans[0])
            cout << k << " ";
        cout << endl;
        end();
        return 0;
    }
}

E

Description

你有一个 $n\times m\space(1 \leq n, m\leq200)$​ 的格子纸,格子要么涂黑(#)要么涂白(.)。你需要用若干个 $1\times n$ ​和 $n\times 1$ ​的长方形去覆盖所有黑色格子,要求不能盖到白色格子上,不能盖到其他长方形上,不能出格子纸的边界,求最小用多少个长方形。

Solution

遇事不决看样例。发现对于一个点,横着的边和竖着的边不能同时选。

选一个边就相当于少了一个矩形,那么我们就要尽可能多选边。

一个常规套路:把边看成点,那么有两种点,且选了一个就不能选另一个,这是啥?不还是二分图吗!

我们把与一个点相邻的相互垂直的边两两连起来。

因为要多选点,也就是说要求二分图最大点独立集,那么只要求最大匹配即可,最大点独立集的大小就是点数减去最大匹配。

就这么简单?确实就这么简单,但还有点小细节:假如一个黑色格子周围没有边,它应该被算进答案,但我们刚刚没算。

那就加上即可,或者直接懒一点:最终答案 = 黑格子数 - 边数 + 最大匹配。

Code

我这个代码感觉加边部分还是很清晰的。

我是用点编号来代替边的编号,很简单。

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
using std::endl;

const int maxn = 100010;

char str[210][210];

struct edge
{
    int x, y, cap, flow;
};

struct Dinic
{
    std::vector<edge> edges;
    std::vector<int> e[maxn];
    inline void add(int x, int y, int cap)
    {
        //cout << "added: " << x << " " << y << endl;
        edges.push_back({x, y, cap, 0});
        edges.push_back({y, x, 0, 0});
        int m = edges.size();
        e[x].push_back(m - 2), e[y].push_back(m - 1);
    }
    int dis[maxn], vis[maxn], cur[maxn], s, t;
    bool bfs()
    {
        std::queue<int> q;
        memset(vis, 0, sizeof(vis));
        q.push(s), vis[s] = 1, dis[s] = 0;
        while (!q.empty())
        {
            int x = q.front();
            q.pop();
            for (int i : e[x])
            {
                auto &k = edges[i];
                if (!vis[k.y] && k.cap - k.flow > 0)
                    dis[k.y] = dis[x] + 1, vis[k.y] = 1, q.push(k.y);
            }
        }
        return vis[t];
    }
    int dfs(int x, int lim)
    {
        if (x == t || lim == 0)
            return lim;
        int res = 0, f;
        for (int &i = cur[x]; i < (int)e[x].size(); i++)
        {
            auto &k = edges[e[x][i]];
            if (dis[k.y] != dis[x] + 1 || (f = dfs(k.y, std::min(lim, k.cap - k.flow))) == 0)
                continue;
            k.flow += f, edges[e[x][i] ^ 1].flow -= f, res += f, lim -= f;
            if (lim == 0)
                break;
        }
        return res;
    }
    int dinic(int s_, int t_)
    {
        s = s_, t = t_;
        int ans = 0;
        while (bfs())
            memset(cur, 0, sizeof(cur)), ans += dfs(s, 1 << 30);
        return ans;
    }
} G;

int n, m;

inline int id(int x, int y) { return (y - 1) * n + x + n * m + 2; }
inline int id2(int x, int y) { return m * (x - 1) + y + 1; }

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        scanf("%s", str[i] + 1);
    int s = 1, t = n * m * 2 + 3, tot = 0, cnt_p = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            if (str[i][j] == '#')
            {
                cnt_p++;
                if (str[i + 1][j] == '#')
                    tot++, G.add(1, id2(i, j), 1);
            }
        }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j < m; j++)
        {
            if (str[i][j] == '#' && str[i][j + 1] == '#')
            {
                //cout << "cur: " << i << " " << j << endl;
                G.add(id(i, j), t, 1), tot++;
                if (i > 1 && str[i - 1][j] == '#')
                    G.add(id2(i - 1, j), id(i, j), 1);
                if (i < n && str[i + 1][j] == '#')
                    G.add(id2(i, j), id(i, j), 1);
                if (i > 1 && str[i - 1][j + 1] == '#')
                    G.add(id2(i - 1, j + 1), id(i, j), 1);
                if (i < n && str[i + 1][j + 1] == '#')
                    G.add(id2(i, j + 1), id(i, j), 1);
            }
        }
    cout << cnt_p - (tot - G.dinic(s, t)) << endl;
}