NOI2022 题解合集
D1T1. P8496 [NOI2022] 众数
视
对于操作 1 和 2,直接用栈维护。
但对于操作 4,栈不支持快速合并,因此考虑双端队列启发式合并,则该部分总复杂度
注意到我们只需要支持尾部插入删除,快速合并,考虑双向链表,容易做到总复杂度
关于绝对众数,有一个非常经典的方法是摩尔投票法,而摩尔投票法是可快速合并的信息,因此考虑对每个队列开一棵动态开点线段树,下标是元素的值,每个下表维护的值即该下标对应的值在队列中的出现次数,pushup 直接摩尔投票,合并时线段树合并即可。
对于操作 3,将所有相关队列进行摩尔投票,并检查最终得到的值是否出现次数严格大于一半,这个直接在维护的线段树上查一下即可。
注意点:特别注意在队列为空时双端队列头尾的处理,否则容易挂分;注意平衡 long long 和空间限制。
时空复杂度
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0;
char s = getchar();
while(!isdigit(s)) s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e6 + 5;
constexpr int K = N * 21;
struct dat {
ll val, cnt; // 注意开 ll
dat operator + (const dat &x) const {
if(val == x.val) return (dat) {val, cnt + x.cnt};
if(cnt >= x.cnt) return (dat) {val, cnt - x.cnt};
return (dat) {x.val, x.cnt - cnt};
}
} val[K];
int node, R[N], ls[K], rs[K];
void modify(int l, int r, int p, int &x, int v) {
if(!x) x = ++node;
if(l == r) {
val[x].val = p;
val[x].cnt += v;
return;
}
int m = l + r >> 1;
if(p <= m) modify(l, m, p, ls[x], v);
else modify(m + 1, r, p, rs[x], v);
val[x] = val[ls[x]] + val[rs[x]];
}
int merge(int x, int y) {
if(!x || !y) return x | y;
ls[x] = merge(ls[x], ls[y]);
rs[x] = merge(rs[x], rs[y]);
return val[x] = val[x] + val[y], x;
}
vector<int> rt;
ll query(int l, int r, int p) {
if(l == r) {
ll ans = 0;
for(int it : rt) ans += val[it].cnt;
return ans;
}
int m = l + r >> 1;
if(p <= m) {
for(int &it : rt) it = ls[it];
return query(l, m, p);
}
else {
for(int &it : rt) it = rs[it];
return query(m + 1, r, p);
}
}
int cnt, hd[N], tl[N], pre[N], nxt[N], dt[N], sz[N];
void push(int x, int v) {
if(!hd[x]) hd[x] = tl[x] = ++cnt;
else nxt[tl[x]] = ++cnt, pre[cnt] = tl[x], tl[x] = cnt;
dt[cnt] = v, sz[x]++;
modify(1, N, v, R[x], 1);
}
void pop(int x) {
int v = dt[tl[x]];
if(hd[x] == tl[x]) hd[x] = tl[x] = 0;
else tl[x] = pre[tl[x]];
sz[x]--;
modify(1, N, v, R[x], -1);
}
void merge(int x, int y, int z) {
if(!hd[x]) hd[z] = hd[y], tl[z] = tl[y];
else if(!hd[y]) hd[z] = hd[x], tl[z] = tl[x];
else hd[z] = hd[x], tl[z] = tl[y], pre[hd[y]] = tl[x], nxt[tl[x]] = hd[y];
sz[z] = sz[x] + sz[y];
R[z] = merge(R[x], R[y]);
}
int n, q;
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("a.in", "r", stdin);
FILE* OUT = freopen("a.out", "w", stdout);
#endif
ios::sync_with_stdio(0);
n = read(), q = read();
for(int i = 1; i <= n; i++) {
int l = read();
while(l--) push(i, read());
}
for(int i = 1; i <= q; i++) {
int op = read();
if(op == 1) {
int x = read();
push(x, read());
}
if(op == 2) pop(read());
if(op == 3) {
int m = read();
rt.clear();
dat ans = {0, 0};
ll tot = 0;
while(m--) {
int x = read();
tot += sz[x];
ans = ans + val[R[x]];
rt.push_back(R[x]);
}
int major = -1;
if(ans.val && query(1, N, ans.val) * 2 > tot) major = ans.val;
print(major), putchar('\n');
}
if(op == 4) {
int x = read(), y = read(), z = read();
merge(x, y, z);
}
}
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 8:28
finish debugging at 8:41
*/
D1T2. P8497 [NOI2022] 移除石子
这道题目比较抽象,比较人类智慧。
对于合法序列计数题,首先必须会判定合法序列,即考虑
从最基本的
-
区间长度要求
等价于区间长度为 ,经典套路。 -
不会进行相同的操作二,可用若干操作一代替。
设计类插头 DP
根据分析,
- 到
长度已经等于 且还要继续延伸到 的位置的区间只可能是 , 和 ,因此 。 - 从每个位置开始的操作二数量
,因此 。
上述做法已经可以通过该档部分分。
进一步地,我们发现
因此,设
类似分析,可知
进一步挖掘性质,我们发现
- 若从一个位置开始同时有长度为
的操作二,可以简化成一次操作一和从下一个位置开始长度为 的操作二。
因此
进一步地,我们枚举跨过
时间复杂度
考虑
-
若
显然不影响。 -
若
,考虑不添加任何石子的有解局面最终方案:- 若存在至少一次操作一则必然有解,直接将石子放在操作一位置上即可。
- 若存在长度
的操作二则必然有解,将石子放在操作二左端变成一次操作一和一次操作二即可。 - 若存在长度
的操作二且 则必然有解,将石子放在操作二两侧某空位上( 故存在),变成一次长度 的操作二即可。
综合上述分析,可知非法当且仅当
且 ,或 。 -
若
:- 若
有解,将两颗石子放在任意位置用一次操作一处理,得出 有解。 - 若
有解,考察所有 添加一颗石子,有解变无解 的局面,即 且 ,或 。去掉一颗石子,要么没有石子可以去掉,即这种情况不可能发生,要么总可以重新添加两颗石子使得有解。
综合上述分析,若局面添加小于两颗石子有解,则添加两颗石子有解。
- 若
-
同理可证对于任意
,若局面添加小于 颗石子有解,则添加 颗石子有解。
特判掉
综上,设
时间复杂度
从
由于之前我们已经对状态进行了充足的简化,
通过对拍我们发现
对于最终态
将对
时间复杂度
总结:D1T2 和 D2T2 都是从简化问题入手,通过结合两个问题维度上的扩展得到正解,做这两道题让我受益匪浅。这让我想到了平行四边形,给它加上对角线相等的条件就成了矩形,给它加上邻边相等的条件就成了菱形,将两者结合在一起就成了正方形。有的时候从平行四边形到正方形是困难的,但从平行四边形到矩形和菱形是自然的,而它们的结合也是自然的。这给出了我们思考问题的一条有效的道路,同时也说明了为什么从部分分开始思考,一步一步打暴力是优秀的。我希望大家都能体会到这一点。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e3 + 5;
constexpr int S = 8765;
constexpr int mod = 1e9 + 7;
void cmin(int &x, int y) {x = x < y ? x : y;}
void add(int &x, int y) {x += y, x >= mod && (x -= mod);}
int n, k, l[N], r[N], node, f[S], g[S], tr[S][7];
map<vector<int>, int> mp;
int dfs(vector<int> cur) {
if(mp.find(cur) != mp.end()) return mp[cur];
int ind = mp[cur] = node++;
for(int a = 0; a < 7; a++) {
vector<int> suc(9, 101);
for(int j = 0; j < 3; j++)
for(int k = 0; k < 3; k++) {
int v = cur[j * 3 + k];
if(v == 101) continue;
for(int st = 0; st < 3; st++) {
int val = a - j - k - st;
int need = val < 0 ? -val : val == 1 ? 1 : 0;
for(int nj = k; nj <= min(2, j + k); nj++) cmin(suc[nj * 3 + st], v + need);
}
}
tr[ind][a] = dfs(suc);
}
return ind;
}
void solve() {
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> l[i] >> r[i];
memset(f, 0, sizeof(f)), f[0] = 1;
for(int i = 1; i <= n; i++) {
memset(g, 0, sizeof(g));
for(int j = 0; j < S; j++) {
if(!f[j]) continue;
for(int a = 0; a < 7; a++) {
int coef = 0;
if(a < 6) coef = l[i] <= a && a <= r[i];
else if(r[i] >= 6) coef = r[i] - max(6, l[i]) + 1;
if(coef == 1) add(g[tr[j][a]], f[j]);
else if(coef > 0) add(g[tr[j][a]], 1ll * f[j] * coef % mod);
}
}
swap(f, g);
}
int ans = 0;
for(auto it : mp) if(it.first[0] <= k) add(ans, f[it.second]);
if(k == 1) {
bool all0 = 1;
for(int i = 1; i <= n; i++) all0 &= !l[i];
if(all0) ans--;
if(n == 3 && l[1] <= 1 && r[1] >= 1 && l[2] <= 1 && r[2] >= 1 && l[3] <= 1 && r[3] >= 1) ans--;
}
cout << (ans % mod + mod) % mod << "\n";
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("b.in", "r", stdin);
FILE* OUT = freopen("b.out", "w", stdout);
#endif
vector<int> I(9, 101);
I[0] = 0, dfs(I);
cerr << node << endl;
int T;
cin >> T;
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/31
author: Alex_Wei
start coding at 17:50
finish debugging at 21:17
*/
D2T1. P8499 [NOI2022] 挑战 NPC Ⅱ
题目已经明示树哈希了,我们需要一个正确的树哈希方法解决树同构问题。
设树哈希函数
对于有根树
。- 存在一种
的 双射 ,使得对于任意 , 。
说人话就是根节点儿子数量相等,且存在两棵树的根儿子的一一对应关系,使得对应儿子子树同构。这样我们就将问题缩小至儿子子树的规模,这给予我们树形 DP 的思路。
具体地,设
因此,我们需要用 具有交换律 的运算结合所有
搞定了树同构,让我们回到原问题。注意到
设
证明:不妨设
因此,不断删去
注意递归处理前需要判一下是否存在
加入上述剪枝后复杂度看起来很对,因为
Upd:实际上复杂度是
好玄学啊这题,树哈希和时间复杂度分析都很玄学。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e5 + 5;
constexpr int base = 131;
constexpr int mod = 1e9 + 7;
int C, T, k, pw[N];
vector<int> G[N], H[N];
struct solver {
vector<int> e[N];
int n, R, f[N], sz[N];
void dfs(int id) {
f[id] = pw[e[id].size()], sz[id] = 1;
for(int it : e[id]) dfs(it), f[id] = 1ll * f[id] * f[it] % mod, sz[id] += sz[it];
f[id] = (f[id] + 19260817) % mod;
sort(e[id].begin(), e[id].end(), [&](int x, int y) {return f[x] < f[y];});
}
void init() {
n = read();
for(int i = 1; i <= n; i++) e[i].clear();
for(int i = 1; i <= n; i++) {
int ff = read();
if(ff != -1) e[ff].push_back(i);
else R = i;
}
dfs(R);
}
} g, h;
map<int, int> f[N];
bool dfs(int x, int y) {
if(g.e[x].size() < h.e[y].size()) return 0;
if(g.sz[x] == h.sz[y]) return g.f[x] == h.f[y];
if(f[x].find(y) != f[x].end()) return f[x][y];
vector<int> gson, hson;
int pg = 0, ph = 0;
auto grt = [&]() {gson.push_back(g.e[x][pg++]);};
auto hrt = [&]() {hson.push_back(h.e[y][ph++]);};
while(pg < g.e[x].size() && ph < h.e[y].size()) {
if(g.f[g.e[x][pg]] == h.f[h.e[y][ph]]) pg++, ph++;
else if(g.f[g.e[x][pg]] < h.f[h.e[y][ph]]) grt();
else hrt();
}
while(pg < g.e[x].size()) grt();
while(ph < h.e[y].size()) hrt();
int cut = g.sz[x] - h.sz[y];
if(gson.size() > cut) return 0;
vector<int> p(gson.size());
for(int i = 0; i < gson.size(); i++) p[i] = i;
do {
bool ok = 1;
for(int i = 0; i < hson.size(); i++) ok &= g.sz[gson[p[i]]] > h.sz[hson[i]];
if(!ok) continue;
for(int i = 0; i < hson.size(); i++) ok &= dfs(gson[p[i]], hson[i]);
if(ok) return f[x][y] = 1;
} while(next_permutation(p.begin(), p.end()));
return f[x][y] = 0;
}
void solve() {
g.init(), h.init();
for(int i = 1; i <= g.n; i++) f[i].clear();
puts(dfs(g.R, h.R) ? "Yes" : "No");
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("d.in", "r", stdin);
FILE* OUT = freopen("d.out", "w", stdout);
#endif
for(int i = pw[0] = 1; i < N; i++) pw[i] = 1ll * pw[i - 1] * base % mod;
C = read(), T = read(), k = read();
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 8:55
finish debugging at 9:14
*/
D2T2. P8500 [NOI2022] 冒泡排序
首先对题目进行初步观察:
- 冒泡排序交换次数就是逆序对数,只能说这个皮套得很离谱。
可以离散化,因为在最终方案中若存在 不等于任何 ,则将其调整到大于它的最小的 或小于它的最大的 均不使得逆序对数量变多。
受到 P4229 某位歌姬的故事 的影响,我在考场上以为这题是神仙 DP 题,不会做,摆烂了。但仔细想想就会发现 P4229 只要考虑最后一个满足条件的位置,但是本题逆序对数需要考虑当前位置和之前所有位置产生的联合贡献,看起来不太能做。
从部分分入手,这题部分分设置得真好。
考虑 特殊性质 B。除去无解的情况,一些位置的值已经固定,其余位置可以任意取值。
性质 1:任意取值的位置必然不产生逆序对。
证明:若
据此,从后往前枚举所有任意取值的位置
注意尽管具有决策单调性,但
进一步地,考虑 特殊性质 C。限制相对独立,我们找一些贪心思想。逆序对数相关,我们希望 较小的数尽可能排在前面。这就启发我们得到如下结论:对于限制
借鉴性质 B 的思路,我们有如下算法:设
这个做法正确性证明有点难,大家根据性质 B 做法感性理解即可。
接下来考虑 特殊性质 A。若
- 进行处理使得区间不相互包含(但注意若
,那么是 限制更紧,要保留 而非 ),然后用指针维护最后一个未满足的区间,扫一遍所有未被填入 的位置。 - 或者将所有区间按照左端点从大到小排序后考虑,用 set 维护已经选中和未被选中的位置。若不存在已经选中的位置属于
则在未被选中的位置集合中查询 的后继,若不存在或大于 则无解,否则选中该后继。
固定一些位置等于
贪心正确性证明:考虑
正解考虑结合特殊性质 A 和特殊性质 C 的做法,对每个值
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
inline int read() {
int x = 0, sgn = 0;
char s = getchar();
while(!isdigit(s)) sgn |= s == '-', s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return sgn ? -x : x;
}
inline void print(int x) {
if(x < 0) return putchar('-'), print(-x);
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
bool Mbe;
constexpr int N = 1e6 + 5;
int n, m, d[N], fa[N], val[N], low[N];
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
struct BIT {
int c[N];
void clear() {for(int i = 1; i <= m; i++) c[i] = 0;}
void add(int x, int v) {while(x <= m) c[x] += v, x += x & -x;}
int query(int x) {int s = 0; while(x) s += c[x], x -= x & -x; return s;}
int query(int l, int r) {return query(r) - query(l - 1);}
} tr;
struct limit {
int l, r, v;
} c[N];
set<int> pos[N];
vector<limit> buc[N];
struct Segtree1 {
struct dat {
int mn, pos;
dat operator + (const dat &x) const { // ensure that pos < x.pos
assert(pos < x.pos);
return mn <= x.mn ? *this : x;
}
} val[N << 2];
int laz[N << 2];
void build(int l, int r, int x) {
laz[x] = 0;
if(l == r) return val[x] = {0, l}, void();
int m = l + r >> 1;
build(l, m, x << 1), build(m + 1, r, x << 1 | 1);
val[x] = val[x << 1] + val[x << 1 | 1];
}
void tag(int x, int v) {val[x].mn += v, laz[x] += v;}
void down(int x) {
if(laz[x]) {
tag(x << 1, laz[x]);
tag(x << 1 | 1, laz[x]);
laz[x] = 0;
}
}
void modify(int l, int r, int ql, int qr, int x, int v) {
if(ql > qr) return;
if(ql <= l && r <= qr) return tag(x, v);
int m = l + r >> 1;
down(x);
if(ql <= m) modify(l, m, ql, qr, x << 1, v);
if(m < qr) modify(m + 1, r, ql, qr, x << 1 | 1, v);
val[x] = val[x << 1] + val[x << 1 | 1];
}
dat query(int l, int r, int ql, int qr, int x) {
if(ql <= l && r <= qr) return val[x];
int m = l + r >> 1;
dat ans = {N, 0};
if(ql <= m) ans = ans + query(l, m, ql, qr, x << 1);
if(m < qr) ans = ans + query(m + 1, r, ql, qr, x << 1 | 1);
return ans;
}
} sgt1;
void solve() {
n = read(), m = read();
for(int i = 1; i <= n + 1; i++) fa[i] = i, val[i] = low[i] = 0;
for(int i = 1; i <= m; i++) {
c[i].l = read(), c[i].r = read();
d[i] = c[i].v = read();
}
sort(d + 1, d + m + 1);
for(int i = 1; i <= m; i++) c[i].v = lower_bound(d + 1, d + m + 1, c[i].v) - d;
for(int i = 1; i <= m; i++) buc[i].clear(), pos[i].clear();
for(int i = 1; i <= m; i++) buc[c[i].v].push_back(c[i]);
for(int i = m; i; i--)
for(auto it : buc[i]) {
while(1) {
int p = find(it.l);
if(p > it.r) break;
fa[p] = p + 1, low[p] = i, pos[i].insert(p);
}
}
for(int i = 1; i <= m; i++) {
sort(buc[i].begin(), buc[i].end(), [&](limit x, limit y) {return x.l > y.l;});
set<int> settle;
for(auto it : buc[i]) {
auto pt = settle.lower_bound(it.l);
if(pt != settle.end() && *pt <= it.r) continue;
pt = pos[i].lower_bound(it.l);
if(pt == pos[i].end() || *pt > it.r) return puts("-1"), void();
settle.insert(*pt);
val[*pt] = i;
pos[i].erase(pt);
}
}
sgt1.build(1, m, 1);
for(int i = 1; i <= n; i++) if(low[i]) sgt1.modify(1, m, 1, low[i] - 1, 1, 1);
for(int i = n; i; i--) {
if(low[i]) sgt1.modify(1, m, 1, low[i] - 1, 1, -1);
if(!val[i]) val[i] = sgt1.query(1, m, low[i], m, 1).pos;
sgt1.modify(1, m, val[i] + 1, m, 1, 1);
}
ll ans = 0;
tr.clear();
for(int i = 1; i <= n; i++) ans += tr.query(val[i] + 1, m), tr.add(val[i], 1);
cout << ans << "\n";
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
#ifdef ALEX_WEI
FILE* IN = freopen("bubble6.in", "r", stdin);
FILE* OUT = freopen("e.out", "w", stdout);
#endif
int T;
cin >> T;
while(T--) solve();
cerr << TIME << " ms\n";
return 0;
}
/*
2022/8/29
author: Alex_Wei
start coding at 16:34
finish debugging at 20:21
*/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现