周赛 Round 138 题解
A 小苯的转盘游戏
知识点:线性不定方程,同余 / 模运算,构造性证明
设进行了 次
,
次
,则最终有:
也就是:
因为 和
互质,且我们可以让
足够大来保证
也非负。
更具体地说:
-
先选一个
,满足
-
再让
足够大,使得
这样的 总能找到,所以任意
都可以互相到达。
时间复杂度
void solve() {
cout << "YES" << endl;
}
B 小苯的最大异或
知识点:位运算,枚举,优化暴力
对 或
做一次操作,相当于:
也就是二进制右移一位:
所以,连续操作若干次后:
可能变成
可能变成
其中 都是非负整数。
因为 和
的操作互不影响,最终只和“各自被操作了多少次”有关,和操作顺序无关。
所以最终所有可能状态一定是:
由于数据范围保证 ,而
。也就是说,最多右移
次左右就会变成
,再往后也还是
。因此枚举
已经完全覆盖所有情况。
我们只要把所有可能的 都试一遍,取最大异或值即可。
时间复杂度
void solve() {
int x, y;
cin >> x >> y;
int ans = 0;
for (int i = 0; i <= 31; ++i) {
int a = x >> i;
for (int j = 0; j <= 31; ++j) {
int b = y >> j;
ans = max(ans, a ^ b);
}
}
cout << ans << endl;
}
C 小苯的数位排序
知识点:贪心,单调性,模拟,构造 / 可行性判断
每次操作把某个数变成它的各位数字和:
这个操作有两个性质:
- 不会变大,只会变小或不变;
- 对于
,一定有
。
所以一个数不断做下去,最终会变成一个一位数,并且之后再怎么做都不会变了。
我们要让整个数组非递减,也就是:
从右往左处理最合适,因为:
- 右边一旦确定,就不应该再动它;
- 左边怎么变,只会影响自己和更左边,不会影响已经处理好的右边。
所以我们从 到
依次处理:
- 如果
,不用管;
- 如果
,就只能不断对
做
,直到:
- 它变得
,或者
- 它已经不再变化了,但还是大于
,这时无解。
- 它变得
时间复杂度
void solve() {
int n;
cin >> n;
auto calc = [&](ll x) {
string s = to_string(x);
ll sum = 0;
for (auto v : s) sum += v - '0';
return sum;
};
vector<ll> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int ans = 0;
for (int i = n - 2; i >= 0; --i) {
while (a[i] > a[i + 1]) {
ll nxt = calc(a[i]);
if (nxt == a[i]) {
cout << -1 << endl;
return;
}
a[i] = nxt;
ans++;
}
}
cout << ans << endl;
}
D 小苯的路径计数
知识点:树形 DP,DFS 遍历,祖先到后代路径计数,按路径终点统计贡献
对于每个节点 ,只需要知道,以
为终点的、满足条件的同色路径有多少条。
设这个数量为 。设
的父亲是
:
-
若
,那么没有任何同色路径能从
延伸到
,所以
-
若
,则:
本身是一条合法路径;
- 所有以
为终点的同色路径,都可以再接上
继续保持同色。
所以:
时间复杂度
void solve() {
int n;
cin >> n;
vector<int> c(n + 1);
for (int i = 1; i <= n; i++) cin >> c[i];
vector<vector<int>> g(n + 1);
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
g[u].pb(v), g[v].pb(u);
}
ll ans = 0;
vector<ll> dp(n + 1);
auto dfs = [&](auto &&self, int u, int fa) -> void {
for (auto v : g[u]) {
if (v == fa) continue;
dp[v] = (c[v] == c[u] ? dp[u] + 1 : 0);
ans += dp[v];
self(self, v, u);
}
};
dfs(dfs, 1, 0);
cout << ans << endl;
}
E 小苯的数字染色
知识点:前缀 DP,状态压缩,按奇偶分类维护最优值
所以整个过程等价于:
- 按顺序选择若干个互不重叠的区间
- 每个区间只看两端点,且两端点数值奇偶性相同
- 目标最大化所有区间端点和
如果我们从左到右扫描数组:
- 当前点可以作为某个区间的左端点
- 也可以作为某个之前左端点的右端点
- 由于区间不能重叠,所以一旦某个区间被选中,它对后面的影响只体现在前缀最优值里
而且这里只和数值的奇偶性有关,所以只需要维护奇数端点和偶数端点两个状态即可。
设:
-
:扫描到当前位置之前,已经处理完的前缀能得到的最大分数
-
:表示“当前有一个还没闭合的左端点”时,能得到的最大值,即:
其中 是某个奇偶性为
的左端点。
扫描到 ,设它的奇偶性为
。
-
不把
作为右端点:
-
把
作为某个同奇偶左端点的右端点:
因为
里已经包含了“左端点 + 左边所有已经完成的区间”的最优值。
-
把
作为新的左端点,以后可能和后面的同奇偶数配对:
这里必须用当前的
,因为左端点前面的部分已经确定最优了。
时间复杂度
constexpr ll INF = 2e18;
void solve() {
int n;
cin >> n;
ll dp = 0;
ll m[2] = {-INF, -INF};
for (int i = 0; i < n; ++i) {
ll a;
cin >> a;
int p = abs(a) % 2;
ll ndp = dp;
if (m[p] != -INF) ndp = max(ndp, a + m[p]);
m[p] = max(m[p], dp + a);
dp = ndp;
}
cout << dp << endl;
}
F 小苯的对称序列
知识点:区间嵌套 DP,按和分类,树状数组,前缀最值优化
对于一个对称子序列,设它首尾配对的和都等于 。那么它的每一对端点都可以看成一个合法配对:
- 普通配对
,要求
,贡献长度
- 中心单点
,要求
,贡献长度
并且这些配对一定是嵌套的:
- 外层配对的左端点更小
- 外层配对的右端点更大
也就是说,合法答案本质上是:
在所有满足 的区间中,找一条左端点递增、右端点递减的最长链。
这就变成了一个经典的二维偏序最长链 / 加权 LIS 问题。
对每个可能的和 ,收集所有满足:
的配对
。
对于某个固定的 ,我们处理这些配对:
- 如果
,
- 如果
,
设当前配对是 ,我们希望找到之前已经处理过的、能套在它里面的最好方案:
- 之前配对的左端点更小
- 右端点更大
也就是找满足: 且
的最大 DP 值。
于是:
其中
把右端点 反过来编号
这样“右端点更大”就变成了 更小,可以用树状数组维护前缀最大值。处理顺序按左端点
从小到大。每个配对
:
- 查询
中所有
的最大值
- 加上当前权值
- 再把结果更新到
对应的位置
时间复杂度
template <class Int = ll>
struct BIT {
vector<Int> a;
int n;
BIT() {}
BIT(int n) { init(n); }
void init(int n) {
this->n = n;
a.resize(n + 1);
}
void update(int x, Int k) {
for (; x <= n; x += x & -x) {
a[x] = max(a[x], k);
}
}
Int ask(int x) {
Int ans = 0;
for (; x; x -= x & -x) {
ans = max(ans, a[x]);
}
return ans;
}
void clear() { fill(a.begin(), a.end(), 0); }
};
void solve() {
int n, mx = 0;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i], mx = max(mx, a[i]);
vector<vector<PII>> pairs(mx * 2 + 1);
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int sum = a[i] + a[j];
pairs[sum].pb({i, j});
}
}
int ans = 0;
BIT tr(n);
for (int S = 2; S <= mx * 2; S++) {
if (pairs[S].empty()) continue;
tr.clear();
int mxl = 0;
for (auto [u, v] : pairs[S]) {
int wt = (u == v) ? 1 : 2;
int val = tr.ask(n - v - 1) + wt;
mxl = max(mxl, val);
tr.update(n - v, val);
}
ans = max(ans, mxl);
}
cout << ans << endl;
}
当然也可以不用树状数组,直接枚举区间内部状态,时间复杂度
void solve() {
int n, mx = 0;
cin >> n;
vector<int> a(n);
vector<vector<int>> pos(1005);
for (int i = 0; i < n; i++) cin >> a[i], mx = max(mx, a[i]), pos[a[i]].pb(i);
vector<bool> flag(2 * mx + 1);
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
flag[a[i] + a[j]] = true;
}
}
int ans = 0;
vector<int> dp(n);
vector<PII> upd;
for (int S = 2; S <= mx * 2; S++) {
if (!flag[S]) continue;
fill(all(dp), 0);
int cur = 0;
for (int i = n - 1; i >= 0; i--) {
int tar = S - a[i];
if (tar < 0 || tar > 1000 || pos[tar].empty()) continue;
upd.clear();
for (auto j : pos[tar]) {
if (j < i) continue;
int in = 0;
for (int k = i + 1; k < j; k++) {
if (dp[k] > in) {
in = dp[k];
}
}
int clen = (i == j) ? 1 : 2 + in;
upd.pb({j, clen});
if (clen > cur) {
cur = clen;
}
}
for (auto [u, v] : upd) {
if (v > dp[u]) {
dp[u] = v;
}
}
}
if (cur > ans) {
ans = cur;
}
}
cout << ans << endl;
}
乱搞 (疑似将要被击杀):
知识点:枚举,区间 DP,滚动数组优化
设选出的对称子序列为
题目要求:
也就是说,这个子序列一定存在一个固定的公共和 ,满足首尾配对都等于
。
所以问题即为,枚举公共和 ,求在原数组中能选出的最长首尾配对和都等于
的子序列长度。
因为 ,所以
的范围只需要枚举到
。
定义 表示在区间
中,能选出的、公共和为
的最长对称子序列长度。
那么转移为:
- 不选左端点
,答案是
- 不选右端点
,答案是
- 如果
,可以把它们作为一对首尾,再加上中间部分
于是:
如果 ,单个数只能作为中心点:
但是 ,最坏情况下肯定会
,所以我们考虑剪枝,先算一个“理论上界”
。对于固定的
:
-
若
,那么值为
和
的元素最多只能配成
-
若
(即
为偶数且
),这些元素可以全部用上,贡献
这就能计算出一个不考虑位置顺序的最大可能长度上界 。
如果 ,说明这个
再怎么做也不可能超过当前答案,直接跳过。
时间复杂度
void solve() {
int n;
cin >> n;
vector<int> a(n + 1), freq(1005);
int mx = 0;
for (int i = 1; i <= n; i++) cin >> a[i], freq[a[i]]++, mx = max(mx, a[i]);
int ans = 0, mxs = mx * 2;
for (int S = 2; S <= mxs; S++) {
int tr = 0;
for (int x = 1; x <= S / 2; x++) {
if (S - x > 1000) continue;
if (x * 2 == S) {
tr += freq[x];
} else {
tr += 2 * min(freq[x], freq[S - x]);
}
}
if (tr <= ans) continue;
vector<int> dp(n + 1, 0);
for (int i = n; i >= 1; i--) {
int prev = 0;
for (int j = i; j <= n; j++) {
int temp = dp[j];
if (i == j) {
dp[j] = (a[i] * 2 == S) ? 1 : 0;
} else {
if (a[i] + a[j] == S) {
dp[j] = 2 + prev;
} else {
dp[j] = max(dp[j], dp[j - 1]);
}
}
prev = temp;
}
}
ans = max(ans, dp[n]);
}
cout << ans << endl;
}
乱搞 :
知识点: ,
优化,优化暴力
固定公共和 以后,把原数组倒过来,记为
,然后把
里的每个数
变成
,得到数组
。那么原数组中最长的、公共和为
的对称子序列等价于
和
的最长公共子序列长度。
所以整题就是:
- 枚举
- 对每个
求一次
- 取最大值。
时间复杂度
static constexpr int MAXB = 8;
struct Bits {
ull b[MAXB]{};
Bits operator|(const Bits& o) const {
Bits r;
#pragma GCC unroll 8
for (int i = 0; i < MAXB; ++i) r.b[i] = b[i] | o.b[i];
return r;
}
Bits and_not(const Bits& o) const {
Bits r;
#pragma GCC unroll 8
for (int i = 0; i < MAXB; ++i) r.b[i] = b[i] & ~o.b[i];
return r;
}
Bits operator-(const Bits& o) const {
Bits r;
unsigned long long borrow = 0;
#pragma GCC unroll 8
for (int i = 0; i < MAXB; ++i) {
borrow = _subborrow_u64(borrow, b[i], o.b[i], &r.b[i]);
}
return r;
}
Bits shl1_or1() const {
Bits r;
ull carry = 1;
#pragma GCC unroll 8
for (int i = 0; i < MAXB; ++i) {
r.b[i] = (b[i] << 1) | carry;
carry = b[i] >> 63;
}
return r;
}
int count(int blocks) const {
int res = 0;
for (int i = 0; i < blocks; ++i) {
res += __builtin_popcountll(b[i]);
}
return res;
}
};
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int blocks = (n + 63) / 64;
ull last = (n % 64 == 0 ? ~0ull : ((1ull << (n % 64)) - 1));
vector<Bits> occ(1001);
for (int j = 0; j < n; ++j) {
int v = a[n - 1 - j];
occ[v].b[j >> 6] |= 1ull << (j & 63);
}
int ans = 1;
for (int S = 2; S <= 2000; ++S) {
Bits D;
for (int x : a) {
Bits M{};
int y = S - x;
if (1 <= y && y <= 1000) M = occ[y];
Bits X = D | M;
Bits Y = D.shl1_or1();
Bits T = X - Y;
D = X.and_not(T);
for (int i = blocks; i < MAXB; ++i) D.b[i] = 0;
D.b[blocks - 1] &= last;
}
ans = max(ans, D.count(blocks));
}
cout << ans << endl;
}
头文件
#include <bits/stdc++.h>
#define pb push_back
#define pf push_front
#define pob pop_back
#define pof pop_front
#define eb emplace_back
#define fi first
#define se second
#define all(a) a.begin(), a.end()
#define rall(a) a.rbegin(), a.rend()
#define endl "\n"
using namespace std;
using ll = long long;
using ld = long double;
using ui = unsigned;
using ull = unsigned long long;
using i128 = __int128;
using PII = pair<int, int>;
using PLL = pair<ll, ll>;
void init() {
}
void solve() {
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
init();
int t = 1;
cin >> t;
cout << fixed << setprecision(15);
for (int _ = 1; _ <= t; ++_) {
solve();
}
return 0;
}

京公网安备 11010502036488号