CSPS2019 题解
CSP-S2025 会赢吗
格雷码
递归即可,注意要开 ull。
括号树
先考虑链上怎么做,是简单的,然后放在树上就是开个栈。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
int n, fa[N], p[N], r[N]; //p[u] : 和 u 匹配的左括号的节点, r[u] : 以 u 结尾的合法子串个数
char s[N];
stack<int>stk;
vector<int>son[N];
ll k[N];
inline void dfs(int pos) {
if (s[pos] == '(') p[pos] = -1, stk.push(pos);
else {
if (stk.empty()) p[pos] = -1;
else p[pos] = stk.top(), stk.pop();
}
r[pos] = p[pos] == -1 ? 0 : r[fa[p[pos]]] + 1;
k[pos] = k[fa[pos]] + r[pos];
for (auto x : son[pos]) dfs(x);
if (s[pos] == '(') stk.pop();
else if (p[pos] != -1) stk.push(p[pos]); //回溯
}
int main() {
scanf("%d", &n);
scanf("%s", s + 1);
for (int i = 2; i <= n; i++) scanf("%d", &fa[i]), son[fa[i]].push_back(i);
dfs(1);
ll ans = 0;
for (int i = 1; i <= n; i++) ans ^= (1ll * i * k[i]);
printf("%lld\n", ans);
return 0;
}
划分
首先大家应该都能想到一个朴素的 dp: \(f_i, j\) 表示最后一段是 \([j, i]\) 时最小的代价,转移显然,时间复杂度 \(\mathcal{O}(n^3)\), 期望得分 \(36\)。
然后考虑优化,发现 dp 没啥用数据结构优化的前途,但是这题是 dp,所以大概率需要我们发现一些题目的性质,得到贪心的方法,从而省去不优的状态。
有如下两个观察:
-
一段和为 \(a + b\) 的数如果切割成一段和为 \(a\) 的与一段和为 \(b\) 的仍满足答案,则由于 \((a + b)^2 > a^2 + b^2\), 且 \(b < a + b\), 不会使后续划分变差,所以这一定是不劣的。
-
如果一段数划分为了一段和为 \(a\) 的以及一段和为 \(b + x\) 的,且 \(a + x \le b\), 我们可以取出 \(b\) 中一段和为 \(x\) 的放入 \(a\) 中,根据 \(a \le b\) 得到 \((a + x) ^ 2 + b^2 < a + (b + x)^2\), 这样调整一定更优。
综合以上观察我们发现:每个 \(i\) 选择的决策点 \(j\)(即划分出 \([j, i]\))一定是最大的满足条件的 \(j\), 因为如果决策点是 \(k\) 且 \(k < j\), 那么完全可以将 \([k + 1, j]\) 这段放在前面而不是划分到 \(i\) 这一段,而放在前面的情况已经在 \(j\) 处考虑过了。如果决策点是 \(k\) 且 \(k > j\), 那么根据观察一多划分肯定更优。
由此,我们设计出了一个 \(\mathcal{O}(n^2)\) 的 dp,记决策点为 \(las_i\), 前缀和数组为 \(s\), 转移为
如何优化?对于上面的条件,移项得到
记 \(val_j = 2s_j - s_{las_j}\), 我们发现如果 \(k < j\) 且 \(val_j \le s_i \lor val_j \le val_k\), \(k\) 就一定不如 \(j\) 优。
这个过程想到了什么?单调队列。只不过是把弹出队首的条件改了一下而已。
最后的统计答案就每次跳 \(las\) 即可。
时间复杂度 \(\mathcal{O}(n)\)。
下面是一些细节:
- 注意开
__int128 - 这题卡空间,
long long类型的数组能且只能开一个,这里只开前缀和数组,将 \(val\) 数组用前缀和 \(\mathcal{O}(1)\) 计算即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef pair<int, int> pii;
const int N = 4e7 + 10, M = 1e5 + 10, mod = (1 << 30) - 1;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int n, typ, a[N], las[N], b[N], m, p[M], q[N];
ll x, y, z, s[N];
ll calc(int i) { return (s[i] << 1) - s[las[i]]; }
int stk[60], top;
void print(i128 num) {
while (num) { stk[++top] = num % 10; num /= 10; }
while (top) cout << stk[top--];
cout << '\n';
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> typ;
if (!typ) {
for (int i = 1; i <= n; i++) cin >> a[i];
} else {
cin >> x >> y >> z >> b[1] >> b[2] >> m;
for (int i = 3; i <= n; i++) b[i] = (x * b[i - 1] + y * b[i - 2] + z) & mod;
for (int i = 1, l, r; i <= m; i++) {
cin >> p[i] >> l >> r;
for (int j = p[i - 1] + 1; j <= p[i]; j++) {
a[j] = b[j] % (r - l + 1) + l;
}
}
}
s[0] = 0;
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
int fr = 1, re = 0; q[++re] = 0;
for (int i = 1; i <= n; i++) {
while (fr < re && calc(q[fr + 1]) <= s[i]) fr++;
las[i] = q[fr];
while (fr <= re && calc(q[re]) >= calc(i)) re--;
q[++re] = i;
}
i128 ans = 0; int pos = n;
while (pos) { i128 num = s[pos] - s[las[pos]]; num = num * num; ans += num; pos = las[pos]; }
print(ans);
return 0;
}
Emiya 家今天的饭
绝对众数:\(k\) 道菜中使用次数 \(> \lfloor\dfrac{k}{2}\rfloor\) 的菜,可能不存在。
考虑以下两个观察:
- 绝对众数是至多只有一个的,所以可以容斥。
- 每种方法最多做一道菜,至多做 \(n\) 道。
于是答案是不考虑有无绝对众数的方案减去有一个数为绝对众数的方案。
枚举作为绝对众数的食材和做的菜数,对于每种方法考虑是否做这种食材即可,由于其它菜是无所谓的,如果不做这道菜的方案数可以前缀和算出。时间复杂度 \(\mathcal{O}(n^3m)\)。
考虑优化,观察到这种食材的出现次数比其它所有食材的出现次数都大,直接把差值记到状态里即可。
时间复杂度 \(\mathcal{O}(n^2m)\)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
// typedef __int128 i128;
typedef pair<int, int> pii;
const int N = 1e2 + 10, M = 2e3 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int n, m, a[N][M];
ll s[M], f[N][N << 1], ans = 1;
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) cin >> a[i][j], s[i] = (s[i] + a[i][j]) % mod;
ans = ans * (s[i] + 1) % mod;
}
ans = (ans + mod - 1) % mod;
for (int i = 1; i <= m; i++) {
f[0][n] = 1;
for (int j = 1; j <= n; j++) {
for (int k = n - j; k <= n + j; k++) {
f[j][k] = (f[j - 1][k + 1] * (s[j] - a[j][i] + mod) % mod + f[j - 1][k - 1] * a[j][i] % mod + f[j - 1][k]) % mod;
}
}
for (int k = 1; k <= n; k++) ans = (ans + mod - f[n][n + k]) % mod;
}
cout << ans << '\n';
return 0;
}
树的重心
首先 \(n\) 为奇数所以只有一个重心 \(rt\),以它为根,对于 \(x \neq rt\),若 \(x\) 成为割掉某边后的重心,则这条边一定不在 \(x\) 子树内,设割掉后另一子树大小为 \(S\),记 \(g_x = \max_{y \in son_x} sz_y\),则有两个限制:
得到:
且要求边不在 \(x\) 子树内,数据结构维护即可。
\(x = rt\) 可以在 dfs 途中处理。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
// typedef __int128 i128;
typedef pair<int, int> pii;
const int N = 3e5 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
struct Query {
int t, l, r;
};
int T, n, cg, sz[N], g[N], dfn[N], idx, m1, m2, in[N];
ll ans;
vector<int>e[N];
vector<Query>q[N];
struct Fenwick {
int tr[N];
#define lowbit(x) (x & -x)
void clear() { for (int i = 1; i <= n; i++) tr[i] = 0; }
void add(int x, int v) {
if (!x) return ;
while (x <= n) {
tr[x] += v;
x += lowbit(x);
}
}
int qry(int x) {
if (x < 0) return 0;
int res = 0;
while (x) {
res += tr[x];
x -= lowbit(x);
}
return res;
}
int ask(int l, int r) {
if (l > r) return 0;
return qry(r) - qry(l - 1);
}
} t1, t2;
inline void dfs1(int u, int fa) {
sz[u] = 1; g[u] = 0;
dfn[u] = ++idx;
for (auto v : e[u]) if (v != fa) {
dfs1(v, u);
sz[u] += sz[v];
g[u] = max(g[u], sz[v]);
}
if (!cg && max(g[u], n - sz[u]) * 2 <= n) cg = u;
}
inline void dfs2(int u, int fa) {
t1.add(sz[fa], -1);
t1.add(n - sz[u], 1);
if (u != cg) {
int l = n - sz[u] * 2, r = n - g[u] * 2;
ans += (ll)u * (t1.ask(l, r) + t2.ask(l, r));
if (in[fa]) in[u] = 1;
if (in[u]) ans += cg * (sz[u] <= n - sz[m2] * 2);
else ans += cg * (sz[u] <= n - sz[m1] * 2);
}
t2.add(sz[u], 1);
for (auto v : e[u]) if (v != fa) {
dfs2(v, u);
}
t1.add(sz[fa], 1);
t1.add(n - sz[u], -1);
if (u != cg) {
int l = n - sz[u] * 2, r = n - g[u] * 2;
ans -= (ll)u * t2.ask(l, r);
}
}
// 这里有个猎奇的点,就是 cg 为根的时候本来给 0 加一是会出问题的,但是这里由于 sz[0] = 0 恰好抵消了,当然我的意思是 xht 的写法是对的,因为 BIT 里给 x++ 了,但是不加还是会似
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> T;
while (T--) {
ans = 0;
cin >> n;
cg = m1 = m2 = 0;
t1.clear(); t2.clear();
for (int i = 1; i <= n; i++) e[i].clear(), q[i].clear(), in[i] = 0;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v); e[v].push_back(u);
}
dfs1(1, 0);
idx = 0;
dfs1(cg, 0);
for (auto v : e[cg]) {
if (sz[v] > sz[m1]) m2 = m1, m1 = v;
else if (sz[v] > sz[m2]) m2 = v;
}
in[m1] = 1;
for (int i = 1; i <= n; i++) t1.add(sz[i], 1);
dfs2(cg, 0);
cout << ans << '\n';
}
return 0;
}
树上的数
https://www.luogu.com.cn/article/s6bg6f12
这篇题解的实现是最短的,然而,个人认为这个毫无注释的代码完全看不懂,所以自己写一篇。
首先枚举当前数字。
考虑菊花图怎么做。我们发现将删的边按顺序连一起,发现每次在中心节点上的数字就构成了一个环,用并查集贪心地维护即可。
然后是链。我们发现一个节点最后的数字从哪边来取决于先删左边还是先删右边,对于每个点记一个优先级即可。具体可以画图理解。
受上述启发得到正解:考虑枚举数字 \(k\), 经过的点依次为 \(u_1, u_2, \cdots, u_m\), 那么有以下三个要求:
- \((u_1, u_2)\) 是 \(u_1\) 的所有边中最早删去的边。
- \((u_{m - 1}, u_m)\) 是 \(u_m\) 的所有边中最迟删去的边。
- 对于 \(i \in [2, m - 1]\), 在 \(u_i\) 的所有边中 \((u_i, u_{i + 1})\) 紧接在 \((u_{i - 1}, u_i)\) 之后删除。
有了这个,仿照菊花图,对每个点开一个并查集,维护优先级,优先级大的是优先级小的祖先,一条边的父亲节点表示对于其和其父亲的公共节点 \(u\), 在与 \(u\) 相邻的所有边中,这两条边的删除顺序是紧挨着的,即其父亲删完后马上删这条。
这里推荐链式前向星存图,方便记边的编号。
注意代码里只开了一个并查集,但是每个点连的边之间是独立的(一条边被拆成了两条有向的)。虚点的意思是,对于一个点连出去的所有边,虚点向最早删除的边连边,最迟删除的边向虚点连边,作用是能让每个点的并查集最后形成一个环,更方便维护信息。
给一个详细的实现。有问题欢迎问我。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 6e3 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
int T, n, res, p[N], fa[N], d[N], sz[N], tot;
int head[N], nxt[N], to[N];
bool in[N], out[N];
void addedge(int u, int v) {
++tot;
nxt[tot] = head[u];
head[u] = tot;
to[tot] = v;
}
int find(int u) { return u == fa[u] ? u : fa[u] = find(fa[u]); }
void merge(int x, int y) { // 连边/合并的是共用一个顶点的两条边所代表的点,且 x -> y 表示这个点所有边的删除顺序中 x 后面紧接着 y
int u = find(x), v = find(y);
if (u == v) return ;
fa[u] = v; sz[v] += sz[u];
out[x] = in[y] = 1;
}
// 由于虚点的存在,最后肯定会连成一个环。
bool check(int x, int y, int lim) { // x, y 之间能否连边
if (in[y] || out[x]) return false; // 已经连过边肯定不合法
int u = find(x), v = find(y);
if (u == v && sz[u] != lim) return false; // 如果需要连边的两条边(两个点) 现在所在的链结尾相同且所有点(包括虚点)都已经在了(size 为 d[u] + 1),就可以连成环了。
return true;
}
inline void dfs1(int u, int f) { // 对于每个点 u,它连的边是并查集需要维护的点,u 是虚点,虚点向起点连边,终点向虚点连边
if (f != u && check(f, u, d[u] + 1)) res = min(res, u);
for (int i = head[u]; i; i = nxt[i]) if (i != f) {
int v = to[i];
if (check(f, i, d[u] + 1)) { // 可以连边就递归下去看看有没有更小的点可以去
dfs1(v, i ^ 1);
}
}
}
inline bool dfs2(int u, int f, int tar) { // 以 tar 为目标
if (u == tar) {
merge(f, u); // 终点向虚点连边
return true;
}
for (int i = head[u]; i; i = nxt[i]) if (i != f) {
int v = to[i];
if (dfs2(v, i ^ 1, tar)) { // 如果 tar 在 v 的子树内,这条边就需要连
merge(f, i);
return true;
}
}
return false;
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= tot; i++) head[i] = in[i] = out[i] = d[i] = 0;
tot = (n & 1) ? n + 2 : n + 1;
// 建了 n 个虚点,边的标号从 tot + 1 开始,但是反边是 ^ 1 所以第一条需要是偶数,所以 n 是奇数则 tot <- n + 2
for (int i = 1; i <= n; i++) cin >> p[i];
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
d[u]++; d[v]++;
addedge(u, v); addedge(v, u);
}
for (int i = 1; i <= tot; i++) fa[i] = i, sz[i] = 1;
for (int i = 1; i <= n; i++) {
int u = p[i];
res = n + 1;
dfs1(u, u);
dfs2(u, u, res);
cout << res << " \n"[i == n];
}
}
return 0;
}

浙公网安备 33010602011771号