CF2119 All(A~F)

Problem A

注意到,\(\oplus\) 运算可能让一个奇数 \(-1\),或者让一个偶数 \(+1\)
转化后的操作如下:

  1. 花费 \(x\) 的代价让 \(a\leftarrow a+1\)
  2. 花费 \(y\) 的代价让 \(a\leftarrow a+1(\text{a is even})\)
  3. 花费 \(y\) 的代价让 \(a\leftarrow a-1(\text{a is odd})\)

现在,我们容易对原题目进行分类讨论:

  • \(a>b+1\):无解
  • \(a=b+1(\text{b is odd})\):无解
  • \(a=b+1(\text{b is even})\):代价为 \(y\)
  • \(a\leq b\)
    \(x\) 为偶数的时候可以选择操作 \(1,2\) 中任意一个,
    \(x\) 是奇数的时候只能选择操作 \(1\)
    \(a,b\) 很小,模拟上述反复 \(+1\) 的过程即可。
点击查看代码
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i)
#define FR(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
int a, b, x, y;
void Solve() {
	scanf("%d %d %d %d", &a, &b, &x, &y);
	if (a > b + 1) {
		puts("-1");
	} else if (a == b + 1 && !(a & 1)) {
		puts("-1");
	} else if (a == b + 1 && (a & 1)) {
		printf("%d\n", y);
	} else {
		int ans = 0;
		for (; a < b; ++a) {
			if (a & 1) {
				ans += x;
			} else {
				ans += min(x, y);
			}
		}
		printf("%d\n", ans);
	}
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		Solve();
	}
	return 0;
}

Problem B

首先,\((p_x,p_y)\)\((q_x,q_y)\) 的具体坐标并没有用,我们关心的只有它们之间的距离,记为 \(d\)

此时容易发现,在 \(n=2\) 时,可以走到距离区间为 \([|a_1-a_2|,a_1+a_2]\)

尝试推广到 \(n\) 更高的情况。具体的,这里将使用增量法解决。
\([l,r]\) 表示,当前可以走到的距离闭区间。假若我们已经考虑了前 \(i-1\) 次操作,现在要移动 \(a_i\) 的距离。

上界 \(r\) 的变化量是好维护的,直接令 \(r\leftarrow r+a_i\)

考虑下界 \(l\) 的变化量,当 \(l\leq a_i\leq r\) 的时候,\(l\) 将会变为 \(0\)
否则,我们令 \(l\leftarrow \min(|l-a_i|,|r-a_i|)\),归纳容易证明,这样必定是最优的。

点击查看代码
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i)
#define FR(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
typedef long double ldb;
const int N = 2e5 + 10;
int n, px, py, qx, qy, a[N];
void Solve() {
	scanf("%d %d %d %d %d", &n, &px, &py, &qx, &qy);
	FL(i, 1, n) {
		scanf("%d", &a[i]);
	}
	ldb l = sqrtl(ldb(px - qx) * (px - qx) + ldb(py - qy) * (py - qy)), r = l;
	FL(i, 1, n) {
		if (l <= a[i] && a[i] <= r) {
			l = 0;
			r += a[i];
		} else {
			l = min(fabsl(l - a[i]), fabsl(r - a[i]));
			r += a[i];
		}
	}
	if (l == 0) {
		puts("Yes");
	} else {
		puts("No");
	}
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		Solve();
	}
	return 0;
}

Poblem C

我们按照 \(n\) 的奇偶性进行初步分类讨论,因为这与 \(\oplus\) 运算很有关。

Part1:\(n\) 是奇数

\(n\) 是奇数的时候,我们可以令答案为 \(a_1=a_2=\dots=a_n=l\)
这样的想法基于 \(\oplus\) 的性质。特别的,只要 \(n\) 个数两两相等,无论如何都是会满足条件的。

Part2:\(n\) 是偶数

考虑 \(n\) 是偶数的情况。注意到,此时 \(a_1\& a_2\& \dots \& a_n\) 的值必须为 \(0\)
证明也比较简单——根据 \(\&\) 的定义,如果存在一个二进制位非 \(0\),那么 \(a_{1\sim n}\) 的这个二进制位必定都是 \(1\)。但是在 \(n\) 是偶数,所以它们的 \(\oplus\) 和为 \(0\),违背了题目条件。

归纳一下,现在需要满足的条件是:

  1. \(a_1\& a_2\& \dots \& a_n\)
  2. \(a_1\oplus a_2\oplus \dots \oplus a_n\)

为了使得字典序最小,我们希望优先让 \(a\) 中靠前的数尽量小。
考虑让 \(a\) 数组中靠前的位置填上尽量多的 \(l\),最后留下 \(2\) 个空位 \(a_{n-1},a_n\)。我们令 \(a_{n-1}=a_n\),且 \(\forall 1\leq i\leq n-2,a_n\oplus a_{i}=0\)

容易发现,可能合法的 \(a_n\) 需要二进制下的最高位比 \(a_1\) 大,并且最高位之后的二进制位全部为 \(0\)

Part3:代码实现

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, l, r, k;
void Solve() {
	scanf("%lld %lld %lld %lld", &n, &l, &r, &k);
	if (n & 1) {
		printf("%lld\n", l);
		return;
	}
	if (n == 2) {
		puts("-1");
		return;
	}
	if (__builtin_clzll(l) == __builtin_clzll(r)) {
		puts("-1");
		return;
	}
	ll t = 1;
	while (t <= l) {
		t <<= 1;
	}
	if (k <= n - 2) {
		printf("%lld\n", l);
		return;
	} else {
		printf("%lld\n", t);
		return;
	}
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		Solve();
	}
	return 0;
}

Problem D

Part1:确定计数方向

定义 \(p_i\) 表示:在\(i\) 次操作中移除标记的位置。如果没有移除标记,则 \(p_i=0\)

假若我们确定了所有 \(p\) 的值,那么确定 \(a\) 的方案数就是所有 \(>0\)\(p_i\) 乘起来(因为我们只要保证 \(a_i\leq p_i\))。也就是说,确定了 \(p\) 数组,就能确定其对应的 \(a\) 方案数。

Part2:整理计数对象

  • 根据题目定义,有:\(p_i\leq i\),即标记不能在操作右边。
  • 我们将决定每个标记,每个操作是否有跟对方匹配。
  • 我们每次要拿一个左侧的标记,去匹配一个右侧的操作。
    所以在任意一个位置,其左边的标记数不能少于操作数。否则必然有一个操作,无法找到左侧的标记去匹配。

Part3:维护方法

考虑按照数轴上的位置从左往右,进行 \(\text{DP}\) 计数。

\(f_{i,j}(j\geq 0)\) 表示,在\(i\) 个位置中\(j\) 个标记还没有找到操作匹配的方案数。

为了方便 \(f\) 的转移,令 \(g_{i,j}(j\geq 0)\) 表示,考虑了前 \(i\) 个位置的所有标记,与前 \(i-1\) 个位置的所有操作,有 \(j\) 个标记还没有找到操作匹配的方案数。

\(f_{i-1}\) 转移至 \(g_{i}\) 时,我们对于 \(i\) 位置的标记是否会匹配一个操作进行分类讨论:

  • 标记 \(i\) 将会与一个操作匹配:
    \(g_{i,j}\leftarrow g_{i-1,j-1}\times i\)
    此处的系数 \(j\) 含义,详见第一部分中的 \(p\) 数组与其方案数。

  • 标记 \(i\) 最后不会与操作匹配:
    \(g_{i,j}\leftarrow f_{i-1,j}\)

类似的,当 \(g_{i}\) 转移至 \(f_{i}\) 时,我们将对于 \(i\) 位置的操作是否匹配一个标记进行分类讨论:

  • 操作 \(i\) 会与一个标记匹配(\(p_i>0\)):
    \(f_{i,j}\leftarrow g_{i,j+1}\times (j+1)\),其中 \(j+1\) 是选择 \(p_i\) 的方案数

  • 操作 \(i\) 不会与某个标记匹配(\(p_i=0\)
    \(f_{i,j}\leftarrow g_{i,j}\)

最终的答案显然是 \(f_{n,0}\),此时所有被选择的标记与操作必须两两匹配。

Part3.5:对本题的一些理解与思考

  • 每个位于左边的标记与位于右边的操作匹配,类似于括号的匹配。
    在这里,标记为 (,操作是 )
  • 我们本质上是在做一个带系数的变种括号匹配。

Part4:代码

点击查看代码
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i)
#define FR(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
typedef long long ll;
const int N = 5010;
int n, MOD, f[N], g[N];
void Solve() {
	scanf("%d %d", &n, &MOD);
	f[0] = 1;
	FL(i, 1, n) {
		fill(g, g + n + 2, 0);
		FL(j, 0, i) {
			g[j] = f[j];
			if (j) {
				g[j] = (g[j] + (ll)f[j - 1] * i) % MOD;
			}
		}
		fill(f, f + n + 2, 0);
		FL(j, 0, i) {
			f[j] = (g[j] + (ll)g[j + 1] * (j + 1)) % MOD;
		}
	}
	printf("%d\n", f[0]);
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		Solve();
	}
	return 0;
}

Problem E

Part1:转化

我们记最终的 \(b\) 序列为 \(b'\),同时令 \(c_i\)\(\geq\) \(b_i\) 的数中最小的,那么 \(c_i\) 满足下面条件

  • \(c_i\) 每一个二进制位不小于 \(a_{i-1}\)
  • \(c_i\) 每一个二进制位不小于 \(a_{i}\)

显然最终的 \(b'_i\geq c_i\),不妨先令所有 \(b_i\leftarrow c_i\),并先累加这部分贡献。

Part2:发现

此时就只要考虑,去掉 \(b_i\oplus b_{i+1}\) 相比于 \(a_i\) 多出来的二进制位。

想要去掉 \(b_i\) 的某些位,就需要选一个更高的 \(0\) 位使其变成 \(1\)
注意到,这里只会选择 \(1\) 个这样的更高位,不然只保留选择中的最高位效果也相同。

Part3:解决问题的算法

我们考虑记录我们选择的“高位”进行 \(\text{DP}\)

\(f_{i,j}\) 为,考虑了前 \(i\) 个数,第 \(i\) 个数我们选择变成 \(1\) 的位是哪个。
每次枚举 \(f_{i-1,k}\),转移到 \(f_{i,j}\),转移合法当且仅当改变后的 \(b_i\oplus b_{i+1}=a_i\)

  • 特别的,你需要将对于每个 \(i\),单独分配一个状态位给“不加任何值”的情况。

时间复杂度 \(O(n\log^2 V)\),其中 \(V\) 是值域。

Part4:代码实现

点击查看代码
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i)
#define FR(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
const ll INF = 1e18;
int n, a[N], b[N], c[N], s[N][32];
ll f[N][32];
void Solve() {
    scanf("%d", &n);
    FL(i, 1, n - 1) {
        scanf("%d", &a[i]);
    }
    a[0] = a[n] = 0;
    FL(i, 1, n) {
        scanf("%d", &b[i]);
        ll t = a[i - 1] | a[i];
        c[i] = t | b[i];
        FR(j, 30, 0) {
            if ((c[i] >> j & 1) && !(t >> j & 1)) {
                if ((c[i] ^ (1 << j)) >= b[i]) {
                    c[i] ^= (1 << j);
                }
            }
        }
        FL(j, 0, 30) {
            s[i][j] = (((c[i] >> j) | 1) << j) | t;
        }
        s[i][31] = c[i];
    }
    {
        FL(i, 0, n) {
            fill(f[i], f[i] + 32, INF);
        }
        FL(i, 0, 31) {
            if (!(b[1] >> i & 1)) {
                f[1][i] = s[1][i] - b[1];
            }
        }
        FL(i, 2, n) {
            FL(j, 0, 31) {
                if (c[i - 1] >> j & 1) {
                    continue;
                }
                FL(k, 0, 31) {
                    if (c[i] >> k & 1) {
                        continue;
                    }
                    if ((s[i - 1][j] & s[i][k]) != a[i - 1]) {
                        continue;
                    }
                    f[i][k] = min(f[i][k], f[i - 1][j] + (s[i][k] - b[i]));
                }
            }
        }
    }
    ll ans = INF;
    FL(i, 0, 31) {
        ans = min(ans, f[n][i]);
    }
    if (ans == INF) {
        puts("-1");
    } else {
        printf("%lld\n", ans);
    }
}
int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        Solve();
    }
    return 0;
}

Problem F

不要被 Div.2 F 这个位置吓到,大部分 F 经过分析其实是简单题。

Part1:一些转化

可以将 \(S\) 视为汽油量,在某些地方会 \(-1\),在某些地方会 \(+1\)
我们称两个相邻且 \(w_u=w_v=1\) 的节点 \(u,v\) 为一个加油站。每次可以花费 \(2\) 的时间在一个加油站获得 \(2\) 单位的汽油量(即在 \(u,v\) 之间往返移动 \(1\) 次)。

Part2:刻画合法路径

容易发现,假若你的路线要经过多个加油站,那么将后面加的油,放到前面的加油站加并不影响答案。这个发现的本质在于你的移动速度和岩浆的蔓延速度一样。
所以,我们的最优路线必定是:

  1. 先经过一些由 \(1,-1,1,-1,\dots\) 交替的路径
    如果出现了 \(-1,-1\) 这样的,那你就死亡了。
  2. 到达一个加油站 \(1,1\),并加够我们后面需要的所有汽油
    如果只有第 \(1\) 步也可以,此时没必要到加油站。
  3. 继续走下去,走到某个目标点 \(U\)
  4. 在目标点和其某个相邻点间,反复移动

我们称上面 \(4\) 段路为 \(path_{1\sim 4}\)

Part3:如何维护这样的路径

对于 \(path_{1\sim 2}\),可以使用 \(\text{BFS}\) 算法,对每个点 \(u\) 预处理出经过 \(path_{1}\) 可以到达的最近的加油站 \(rd_u\)
对于 \(path_3\),你可以从初始点 \(st\) 开始,搜索所有的 \(U\),并计算相应的贡献。
具体的,我们在搜索的时候应该记录:

  • \(U\) 距离 \(st\) 的长度:\(d\)
  • \(st\) 走到 \(U\) 的过程中,汽油的最大缺口量:\(need\)
    这将决定我们在加油站加多少油。
  • 到最近的加油站,需要走的距离:\(recent\text{ }dis\)

遍历到某个 \(U\) 时,我们容易维护我们最优策略所需时间,用它与当前节点的被岩浆淹没的时间比较即可。
假若能走到当前点,则 \(path_{1\sim 4}\) 的总长度就是 \(\text{dist}(1,U)\) 或者 \(\text{dist}(1,U)-1\)。经过归纳,我们取 \(\text{dist}(1,U)-1\) 当且仅当 \(\text{dist}(1,st)\) 是奇数。

Part4:代码实现

点击查看代码
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i)
#define FR(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
const int N = 5e5 + 10, INF = 1e9;
int n, st, ans, w[N], rd[N], dep[N];
vector<int> G[N];
void Dfs1(int u, int fa) {
    for (int v: G[u]) {
        if (v != fa) {
            dep[v] = dep[u] + 1;
            Dfs1(v, u);
        }
    }
}
void CalcRecent() {
    queue<int> que;
    fill(rd, rd + n + 1, INF);
    FL(u, 1, n) for (int v: G[u]) {
        if (w[u] == 1 && w[v] == 1) {
            rd[u] = 0, que.emplace(u);
            break;
        }
    }
    while (!que.empty()) {
        int u = que.front();
        que.pop();
        for (int v: G[u]) {
            if (w[v] != w[u] && rd[v] == INF) {
                rd[v] = rd[u] + 1;
                que.emplace(v);
            }
        }
    }
}
void Dfs2(int u, int fa, int d, int s, int need, int rec_d) {
    if (!(need = max(need, -s))) {
        rec_d = min(rec_d, rd[u]);
    }
    int t = ((need + 1) / 2 + rec_d) * 2;
    if (d + (need? t : 0) < dep[u]) {
        ans = max(ans, dep[u] - (dep[st] % 2 == 0));
    }
    for (int v: G[u]) {
        if (v != fa) {
            Dfs2(v, u, d + 1, s + w[v], need, rec_d);
        }
    }
}
void Solve() {
    scanf("%d %d", &n, &st);
    FL(i, 1, n) {
        scanf("%d", &w[i]);
        G[i].clear();
    }
    FL(i, 1, n - 1) {
        int u, v;
        scanf("%d %d", &u, &v);
        G[u].emplace_back(v);
        G[v].emplace_back(u);
    }
    Dfs1(1, 0);
    CalcRecent();

    ans = 0;
    Dfs2(st, 0, 0, w[st], 0, INF);
    printf("%d\n", ans);
}
int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        Solve();
    }
    return 0;
}
posted @ 2025-07-06 12:15  徐子洋  阅读(37)  评论(1)    收藏  举报