2023 杭电多校 7 1008

HEX-A-GONE Trails

题目大意

给定一棵无根树,两个玩家$(X, Y)$在上面移动,双方只能移动到与自己相邻且没有被任何玩家走过的节点,无法移动的玩家输掉游戏。

题解

首先需要对这个博弈模型进行一些化简,首先我们将 $X, Y$ 之间的路径标记出来,并求出这些路径上的点的价值,这里所谓的价值是以该点为根,不经过 $X, Y$ 路径上的边能够达的点的深度的最大值。例如下面这图能够如此化简:

1

现在用两个指针表示出两个玩家所在的位置,那么可以计算出两个玩家所获得的值(值为所在位置的值加上与初始点的距离的和),以及胜负关系(玩家一获得的值严格大于玩家而获得的值为胜)。

2

可以用当前局面的胜负关系来判断两个指针的移动方向,例如在上图的状态中,玩家一获得的值显然小于玩家二的值,那么如果玩家一想要胜利,那么 $i$ 指针就必须向靠近 $x$ 初始点的方向移动,那么玩家二的选择实际上就成了一个区间。

3

不断重复上述过程,直到两个指针移出被标记的点。

因为对于一个玩家来说,指针到达过的地方不会再次经过,因此可以用链表来维护 $i, j$ 两个指针的前驱和后继,区间的最值可以直接通过前缀得到,不需要数据结构。

时间复杂度 $O(n)$。

参考代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
void solve() {
    int n;
    cin >> n;
    vector<vector<int>> tre(n);
    vector<int> path, col(n);
    int x, y;
    cin >> x >> y;
    x --, y --;
    for(int i = 0; i < n - 1; i ++) {
        int u, v;
        cin >> u >> v;
        u --, v --;
        tre[u].push_back(v);
        tre[v].push_back(u);
    }
    auto dfs = [&](auto self, int u, int f) -> bool {
        if(u == y) {
            path.push_back(u), col[u] = true;
            return true;
        }
        for(int v : tre[u]) if(v != f) {
            if(self(self, v, u)) {
                path.push_back(u), col[u] = true;
                return true;
            }
        }
        return false;
    };
    dfs(dfs, x, x);

    reverse(path.begin(), path.end());
    int w = path.size();
    
    vector<int> val(n), dep(n);
    auto get_v = [&](auto self, int u, int f) -> int {
        dep[u] = dep[f] + 1;
        int res = 0;
        for(auto v : tre[u]) if(v != f && !col[v]) {
            res = max(res, self(self, v, u));
        }
        res ++;
        return res;
    };
    for(int i : path) {
        val[i] = get_v(get_v, i, i);
    }
    
    int mxx = 0, mxy = 0, curx = 0, cury = w - 1;
    for(;curx  + 1 < cury;) {
        if(curx + 1 < cury) curx ++;
        if(cury - 1 > curx) cury --;
    }
    mxx = val[path[curx]] + curx;
    mxy = val[path[cury]] + w - 1 - cury;

    bool res = mxx > mxy;
    vector<int> nxtA(w, -1), preA(w, -1), nxtB(w, -1), preB(w, -1);
    for(int i = 0; i < w - 1; i ++) {
        nxtA[i] = i + 1;
        preB[i] = i + 1;
    }
    for(int i = w - 1; i >= 1; i --) {
        preA[i] = i - 1;
        nxtB[i] = i - 1;
    }
    auto del =[&] () {
        if(nxtA[curx] != - 1) preA[nxtA[curx]] = preA[curx];
        if(preA[curx] != - 1) nxtA[preA[curx]] = nxtA[curx];
        if(nxtB[cury] != - 1) preB[nxtB[cury]] = preB[cury];
        if(preB[cury] != - 1) nxtB[preB[cury]] = nxtB[cury];
    };
    while(cury != -1 && curx != -1) {
        if(res) {
            del();
            curx = nxtA[curx];
            cury = preB[cury];
            if(cury == -1) {
                break;
            }
            mxx = max(mxx, val[path[curx]] + curx);
            mxy = val[path[cury]] + w - 1 - cury;
        } else {
            del();
            cury = nxtB[cury];
            curx = preA[curx];
            if(curx == -1) {
                break;
            }
            mxx = val[path[curx]] + curx;
            mxy = max(mxy, val[path[cury]] + w - 1 - cury);
        }
        res = mxx > mxy;
    }
    cout << (res ? 1 : 0) << '\n';
}

2023 牛客多校 4 H

Merge the squares!

题目大意

给定一个 $n \times n$ 个 $1 \times 1$ 的小正方形组成的一个大正方形。

要求每次选取不超过 $50$ 个正方形拼接成一个新大正方形,最终需要得到一个 $n \times n$ 的正方形。

输出一种可行的方案。

题解

考虑分治的做法。

对于一个正方形,可以分割成如下图的形式:

image1

其中 $A, D$ 是两个正方形,$B, C$ 是两个大小相等的矩形。

显然对于正方形 $A, D$ 可以直接递归处理,处理比较棘手的是两个矩形该如何处理。

对于一个矩阵而言,分割成最少的正方形的方案是对其长宽进行辗转相减,那么可以预处理出对于任意一个矩阵能够分成的正方形数量。

我们可以枚举 $B, C$ 的长宽,找到分割最少的矩阵,分治这个矩阵即可。

当然如果确定了矩形的长宽,这四个矩形可以任意摆放,例如另一种等价的摆放方式:

image2

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
int g[MAXN][MAXN];

vector<tuple<int, int, int>> res;

int dp(int x, int y) {
    if(x == y) {
        return g[x][y] = 1;
    }
    if(g[x][y]) return g[x][y];
    if(x > y) {
        return g[x][y] = 1 + dp(x - y, y);
    } else {
        return g[x][y] = 1 + dp(x, y - x);
    }
}

void div2(int x1, int y1, int x2, int y2);

void div1(int x1, int y1, int x2, int y2) {
    int len = x2 - x1 + 1;
    if(len <= 7) {
        if(len > 1) {
            res.push_back({x1, y1, len});
        }
        return;
    }
    int div = 0, mxcnt = INF;
    for(int i = 1; i <= len / 2; i ++) {
        int x = dp(i, len - i);
        if(x < mxcnt) {
            mxcnt = x, div = i; 
        }
    }
    len = div;
    div1(x1, y1, x1 + len - 1, y1 + len - 1);
    div1(x1 + len, y1 + len, x2, y2);
    div2(x1 + len, y1, x2, y1 + len - 1);
    div2(x1, y1 + len, x1 + len - 1, y2);

    res.push_back({x1, y1, x2 - x1 + 1});
}

void div2(int x1, int y1, int x2, int y2) {
    int lenx = x2 - x1 + 1, leny = y2 - y1 + 1;
    if(lenx == 1 || leny == 1) {
        return;
    }
    if(lenx == leny) {
        div1(x1, y1, x2, y2);
        return;
    }
    if(lenx > leny) {
        div1(x1, y1, x1 + leny - 1, y1 + leny - 1);
        div2(x1 + leny, y1, x2, y2);
    } else {
        div1(x1, y1, x1 + lenx - 1, y1 + lenx - 1);
        div2(x1, y1 + lenx, x2, y2);
    }
}

void solve() {
    int n;
    cin >> n;
    div1(1, 1, n, n);
    cout << res.size() << '\n';
    for(auto [x, y, len] : res) {
        cout << x << " " << y << " " << len << '\n';
    }
}

Harmony in Harmony

题目大意: $n$个人将大小为$1$的地均分两次, 第一次每人分得$1/n$, 第二次同样分为大小相同的$n$份, $n$人自行分配, 要求使得第一次和第二次区域的交集最小的人的值最大, 第二次均分地时有一种分法, 使得这个值最小, 求出这个值.

首先可以猜测此题的答案存在一个上界$\frac{1}{n^2}$. 可以将此过程看作一个完全二分图匹配, 我们可以列出一个$n\times n$的表格, 其中每行的值之和为$\frac{1}{n}$, 每列的值之和为$1$, 所求的值为表格中坐标为 $(i, i)$ 的最小值. 我们假定 $(i, i)$ 为我们要找的最小值, 那么为了让这个值最小, 可以让 $\sum_{k = 1}^{i - 1}(k, i)$ 最大, 又因为要让每人分得的土地最大, 可以让前$i-1$个人分得的土地一样大都为$\frac{1}{in}$, 那么$i$分得的土地大小应为$\frac{\frac{1}{n}-\frac{i-1}{in}}{n-i+1}$, 验证此值小于等于$\frac{1}{n^2}$, 故枚举 $i$ 求出答案.

参考代码:

1
2
3
4
5
6
7
8
9
int main () {
    int n;
    scanf("%d", &n);
    double res = 1;
    for(int i = 0; i <= n; i ++) {
        res = min(res, 1.0 / (n * (n + 1 - i) * i));
    }
    printf("%.9f", res);
}

Paimon Sorting

题目大意: 给出一种$n^2$的排序方式, 询问数组长度为$k$时, 在排序的过程中交换了几次$k\in [1, n]$. 假设我们已经知道长度为$x$的结果, 那么怎么样可以推出$x + 1$的结果就是此题的关键. 设已经出现的最大值为$mx$, 考虑下面几种情况: $a_x < mx$; $a_x = mx$; $a_x > mx$. 首先考虑第一种情况, 注意到该排序每轮是将最大值交换到$i$位置, 因此对于这种情况只需要考虑最后一轮, 从$1$到$x$位置,每次遇到大于$a_x$的数便交换, 实际交换的次数为大于$a_x$的数的集合的大小. 其次考虑第三种情况, 首先$a_x$会走所有前一个最大值所有经过的路径, 此过程的价值为$0$, 其次$a_x$ 会被移动到$1$位置, 再被移动到$x$位置, 此段的价值为 $2$. 最后考虑第二中情况, 显然它本身的价值应该为$0$, 但是它会对下一个 mx 移动到末位置产生阻碍(增加那一段的价值).这段增量为此时的位置到下一个mx位置的长度.

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct BIT {
    int N;
    vector<int> c;
    BIT(int n) {
        N = n;
        c.resize(N);
    };
    void add(int x) {
        for(int i = x; i < N; i += (i & (-i))) {
            c[i] += 1;
        }
    }
    ll sum(int x) {
        ll res = 0;
        for(int i = x; i; i -= (i & (-i))) {
            res += c[i];
        }
        return res;
    }
    ll sum(int l, int r) {
        return sum(r) - sum(l - 1);
    }
};
void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for(int i = 0; i < n; i ++) {
        cin >> a[i];
    }
    ll res = 0;
    BIT bit(n + 10);
    vector<int> tol(n + 1);
    int len = -2;
    int mx = 0;
    for(int i = 0; i < n; i ++) {
        if(mx < a[i]) {
            res += 2 + len;
            mx = a[i];
            len = 0;
        } else if(mx == a[i]) {
            len ++;
        } else {
            if(len) len ++;
        }
        res += bit.sum(a[i] + 1, n + 5);
        if(!tol[a[i]]) {
            tol[a[i]] = 1;
            bit.add(a[i]);
        }
        cout << res;
        if(i == n - 1) {
            cout << '\n';
        } else {
            cout << ' ';
        }
    }
}

Exercise

题目大意: 给定$2n$个数, 原本(1, 2), (3, 4) … 是一组, 现在要重新分组, 但不能维持原来的组, 问重新分组后每组两个值差的绝对值和的最小值.

显然从小到大排序之后, 相邻的两个数分为一个组会使得绝对值和最小, 但不一定符合条件, 于是我们希望能通过某种方法使得所有不合法的组都合法. 发现需要被最优的重组方式一定是与相邻的组进行重组, 且增量为$2(p_{i + 1,l} - p_{i,r})$. 可以考虑一种这样的DP, $dp_{i,j}$, 其中$i$表示考虑到第$i$组, $j$表示该组是否需要被重组. 可以写出转移方程: $$ \begin{matrix} dp_{i, 1} = dp_{i-1, 0}\ dp_{i, 0} = min(dp_{i-1,0},\ dp_{i,0},\ dp_{i,1} + 2(p_{i + 1,l} - p_{i,r})) \end{matrix} $$

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const ll INF = (1ll << 60);
struct PAIR {
    ll l, r;
    bool bad = 0;
};
void solve() {
    int n;
    cin >> n;
    vector<PII> a(2 * n);
    for(int i = 0; i < 2 * n; i ++) {
        cin >> a[i].first;
        a[i].second = i + 1;
    }
    sort(a.begin(), a.end());
    ll res = 0;
    vector<PAIR> p(n);
    for(int i = 0; i < n; i ++) {
        p[i].l = a[2 * i].first;
        p[i].r = a[2 * i + 1].first;
        res += p[i].r - p[i].l;
        int mx = max(a[2 * i].second, a[2 * i + 1].second);
        int mi = min(a[2 * i].second, a[2 * i + 1].second);
        if(mx - 1 == mi && (mx % 2 == 0)) {
            p[i].bad = 1;
        }
    }
    vector<array<ll, 2>> dp(n);
    dp[0][0] = dp[0][1] = INF;
    dp[0][p[0].bad] = 0;
    for(int i = 1; i < n; i ++) {
        dp[i][0] = dp[i][1] = INF;
        dp[i][p[i].bad] = dp[i - 1][0];
        dp[i][0] = min(dp[i][0], min(dp[i - 1][0], dp[i - 1][1]) + 2 * (p[i].l - p[i - 1].r));
    }
    res += dp[n - 1][0];
    cout << res << '\n';
}

Fortune over Sportsmanship

题目大意: $n$人参见一个网球比赛, 一个$n\times n$的方格表示$(i, j)$进行比赛时收获的人气, 比赛总共进行$n - 1$轮, 胜方获得败方的人气(二者取max), 且败方淘汰出局.现在希望求出比赛可能收获的人气最大值, 并找出赛程.

考虑一种贪心的过程, 每次使当前状态下人气最大的两名选手进行比赛, 因为会继承败方的人气, 所以无论哪名选手淘汰都没关系. 可以用优先队列来模拟比赛.

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
struct Z {
    int val;
    int x, y;
    bool operator< (const Z &a) const {
        if(val != a.val) {
            return val < a.val;
        } else {
            return x < y;
        }
    }
};
void solve() {
    int n;
    cin >> n;
    ll res = 0;
    vector<vector<int>> p(n, vector<int> (n));
    vector<bool> over(n);
    vector<PII> op;
    for(int i = 0; i < n; i ++) {
        for(int j = 0; j < n; j ++) {
            cin >> p[i][j];
        }
    }
    priority_queue<Z> q;
    for(int i = 0; i < n; i ++) {
        for(int j = i + 1; j < n; j ++) {
            q.push({p[i][j], i, j});
        }
    }
    while(q.size()) {
        int v = q.top().val;
        int x = q.top().x;
        int y = q.top().y;
        q.pop();
        if(!over[x] && !over[y]) {
            op.push_back({x, y});
            res += v;
            over[y] = 1;
            for(int i = 0; i < n; i ++) {
                if(over[i] || i == x) continue;
                p[i][x] = p[x][i] = max(p[x][i], p[y][i]);
                q.push({p[x][i], min(x, i), max(x ,i)});
            }
        }
    }
    cout << res << '\n';
    for(auto &[x, y] : op) {
        cout << x + 1 << ' ' << y + 1 << '\n';
    }
}

Gaining Rating

题目大意: Monocarp 想提高自己在某下棋网站上的rating, 希望从$x$上升到$y$. 现在有$n$个对手, 每个对手有相应的rating $a_i$, rating 变化的规则为, 获胜加$1$, 失败减$1$但是对手的rating不变, Monocarp 只能下赢rating不大于自己的对手, 同时他可以自己挑选对手, 但是任意两个对手进行对弈的次数差不能大于$1$. 问需要多少轮才能达到目标或者说是不可能的.

显然, 首先需要对所有对手的rating进行一次排序. 我们考虑模拟一轮获得增量, 首先能够赢过$t$个人, 再败给$n-t$个人, 一轮总共获得的分数为$2t-n$, 如果距离所需达成的最终目标小于等于$t$时, 我们便可以不进入下一轮, 反之, 如果这一轮获得的分数小于等于$0$时, 无论如何都无法达到目标. 我们考虑如何求出$t$, 显然直接对$a$进行upper_bound是错误的, 因为我们的分数是在变化的. 可以考虑维护一个$b_i = a_i - i$表示想一路赢到$i$所需的最小rating. 但是这样的$b$不是单调的, 我们将不单调的$b_i$向下合并, 并增设权值, 这样我们得到了一个可以$log$求出$t$的数组.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sort(a.begin(), a.end());
vector<ll> b = a;
for(int i = 0; i < n; i ++) {
    b[i] -= i;
}
vector<int> s(1, 1);
vector<ll> v(1, b[0]);
for(int i = 1; i < n; i ++) {
    if(b[i] <= v.back()) {
        s.back() ++;
    } else {
        v.push_back(b[i]);
        s.push_back(1);
    }
}
v.push_back(1e18);
for(int i = 1; i < s.size(); i ++) {
    s[i] += s[i - 1];
}

上述代码中$v$表示所说的单调数组, $s$表示增量, 接下来我们还不能一下子求出最后的答案, 我们将$v$中数值的大小视为分段的分界线, 已经知道了当前一轮所获得的增量, 我们要判断增量发生变化的值与$y-s_i$哪一个先到, 依次模拟每个分段便能求出最后的答案.

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void solve() {
    int n;
    ll x, y;
    cin >> n >> x >> y;
    vector<ll> a(n);
    for(int i = 0; i < n; i ++) {
        cin >> a[i];
    }
    sort(a.begin(), a.end());
    vector<ll> b = a;
    for(int i = 0; i < n; i ++) {
        b[i] -= i;
    }
    vector<int> s(1, 1);
    vector<ll> v(1, b[0]);
    for(int i = 1; i < n; i ++) {
        if(b[i] <= v.back()) {
            s.back() ++;
        } else {
            v.push_back(b[i]);
            s.push_back(1);
        }
    }
    v.push_back(1e18);
    for(int i = 1; i < s.size(); i ++) {
        s[i] += s[i - 1];
    }
    for(int i = 1; i < s.size(); i ++) {
        s[i] += s[i - 1];
    }
    ll res = 0;
    while(1) {
        int i = upper_bound(v.begin(), v.end(), x) - v.begin();
        if(!i) {
            cout << -1 << '\n';
            return ;
        }
        if(x + s[i - 1] >= y) {
            cout << res + y - x << '\n';
            return ;
        } else {
            int add = 2 * s[i - 1] - n;
            if(add <= 0) {
                cout << -1 << '\n';
                return;
            } else {
                ll ter = min(y - s[i - 1], v[i]);
                ll cnt = (teg - x + add - 1) / add;
                res += cnt * n;
                x += cnt * add;
            }
        }
    }
}

https://codeforces.com/contest/1754/problem/F https://codeforces.com/contest/822/problem/C https://codeforces.com/contest/229/problem/D https://codeforces.com/contest/1288/problem/D https://codeforces.com/gym/104114/problem/G https://codeforces.com/gym/103446/problem/J https://codeforces.com/gym/103470/problem/I

https://codeforces.com/gym/103447/problem/G https://codeforces.com/gym/104023/problem/J https://codeforces.com/gym/104023/problem/D