NOIOL2022 题解
tg 组
昨天 vp 了一下,确实在 3.5h 内做完了,但是 T2 挂成了 40/dk
T1 - 丹钓战
其实不需要关心这个弹栈条件是什么。
注意到,如果固定 \(l\) 的话,那么 \([l,n]\) 内每个元素是否成功就是固定的。于是我们考虑从后往前推 \(l\),每次加入 \((a_l,b_l)\),维护每个元素的成功性,然后就是个区间计数(其实是前缀,因为 \([1,l)\) 不用考虑)。
考虑加入 \((a_l,b_l)\) 后,哪些元素的成功性会发生改变。之前不成功的,在前面加一个元素显然也不会成功。考虑一个之前成功的元素 \(x\),它把区间 \((l,x)\) 弹完之后有机会解决 \(l\),如果解决了,有可能是在之前就被弹了,有可能是它弹掉了 \(l\)。所以如果找到所有之前成功的元素中最左边的能弹 \(l\) 的,那么前面的全部变成不成功,\(l\) 变成成功,其它不变。
并不需要二分找到最左边的,只需要暴力每次找到第一个成功的位置,如果能弹就 break
,否则就赋为不成功继续找,因为每个元素只会被从成功变为不成功一次,通过势能分析法知道复杂度是对的。用 BIT(+ BIT 倍增)即可维护。
code
constexpr int N = 5e5 + 10;
int n, qu;
int a[N], b[N];
vii qry[N];
int ans[N];
struct bitree {
int cnt[N];
void add(int x, int v) {
while(x <= n) cnt[x] += v, x += lowbit(x);
}
int Cnt(int x) {
int res = 0;
while(x) res += cnt[x], x -= lowbit(x);
return res;
}
int fst() {
int x = 0;
PER(i, 20, 0) if(x + (1 << i) <= n && !cnt[x + (1 << i)]) x += 1 << i;
return x + 1;
}
} bit;
void mian() {
n = read(), qu = read();
REP(i, 1, n) a[i] = read();
REP(i, 1, n) b[i] = read();
REP(i, 1, qu) {
int l = read(), r = read();
qry[l].pb(r, i);
}
PER(j, n, 1) {
while(true) {
int i = bit.fst();
if(i == n + 1) break;
if(a[i] != a[j] && b[i] < b[j]) bit.add(i, -1);
else break;
}
bit.add(j, 1);
for(tii q : qry[j]) ans[Y(q)] = bit.Cnt(X(q));
}
REP(i, 1, qu) prt(ans[i]), pc('\n');
}
T2 - 讨论
这个题是唯一有意思的题。
如果不存在,那就是经典的「任意两个集合要么不交,要么包含」的局面。众所周知,这样找到真包含每个集合的极小集合当其父亲(如果不存在,则找全集),这样得到一棵以全集为根的树,满足祖先包含后代,非祖先后代关系则不交。
有一个小问题,就是这个模型必须保证没有相等集合。那只需要去重即可。考虑集合哈希然后放到 unordered_map
。集合哈希的一个经典方法是给每个数随机赋 ull
权,然后异或、求和组成一个 pair<ull, ull>
。还要放到 unordered_map
,所以还要对这个 pair
进一步哈希。就在这一步 wsbl,取了两维的异或和,殊不知当集合大小为 \(1\) 时,这个哈希得到的值一定是 \(0\),就寄了,T 成了 40pts。其实把两维相加,或者干脆就取其中一维当哈希值就行了。在这个题上 STL 再一次吊打了 pbds,果然还是 STL 靠谱!
然后就有了一个思路的轮廓:尝试建树,如果中途有父亲的冲突则 exit。建完树,看是否满足父亲包含儿子、兄弟之间无交,不满足则 exit。
考虑枚举一个元素,然后找到所有包含它的集合,枚举量是 \(\mathrm O(m)\)。按照集合大小排序,如果有大小相同,这两者肯定不符合,直接 exit。那么如果符合模型,易证一定是 \(i\) 的父亲就是 \(i+1\),最大的集合的父亲是全集,这样每个枚举到的集合都会被赋父亲。如果 \(x\) 的父亲信息冲突了,那么 \(x\) 必定与两个父亲之间的一个不符合,exit。
这样一番操作建出了树。事实上我们可以证明,到这一步一定符合模型了。如果不然,一定在上述过程中存在 \(i\) 与 \(i+1\) 不符合,如果如此,那么必定存在元素使得 \(i\) 包含而 \(i+1\) 不包含,此时 \(i\) 的父亲信息显然会发生冲突。
code
constexpr int N = 1e6 + 10;
int n;
vi can[N];
vi mas[N];
int fa[N];
mt19937 rng(19260817);
ull key[N];
struct _hash {
ull sum, xsm;
_hash() { sum = xsm = 0; }
_hash(const vi &v) {
sum = xsm = 0;
for(int x : v) sum += key[x], xsm ^= key[x];
}
bool operator==(const _hash &h) const { return sum == h.sum && xsm == h.xsm; }
};
struct hash_hash {
size_t operator()(const _hash &h) const { return h.sum + h.xsm; }
};
void deal(int id) {
static bool b[N];
vi &v = can[id];
for(int x : v) b[x] = true;
int ans = 0;
PER(i, n, 1) if(i != id) {
vi &w = can[i];
int c = 0;
for(int x : w) c += b[x];
if(c && c < SZ(v) && c < SZ(w)) ans = i;
}
for(int x : v) b[x] = false;
printf("YES\n%d %d\n", id, ans);
}
void mian() {
n = read();
REP(i, 0, n + 1) can[i].clear(), mas[i].clear(), fa[i] = -1, key[i] = uniform_int_distribution<ull>(0, -1ull)(rng);
REP(i, 1, n) {
int m = read();
while(m--) {
int x = read();
can[i].pb(x);
}
sort(ALL(can[i]));
can[i].resize(unique(ALL(can[i])) - can[i].begin());
}
gp_hash_table<_hash, bool, hash_hash> mp;
REP(i, 1, n) {
_hash h(can[i]);
bool &b = mp[h];
if(b) can[i].clear();
else mp[h] = true;
}
REP(i, 1, n) for(int x : can[i]) mas[x].pb(i);
REP(d, 1, n) {
vi &v = mas[d];
if(v.empty()) continue;
sort(ALL(v), [&](const int &x, const int &y) { return SZ(can[x]) < SZ(can[y]); });
REP(i, 0, SZ(v) - 2) {
int x = v[i], y = v[i + 1];
if(x == y) debug(v);
if(SZ(can[x]) == SZ(can[y])) return printf("YES\n%d %d\n", x, y), void();
if(~fa[x] && fa[x] != y) return deal(x), void();
fa[x] = y;
}
if(~fa[v.back()] && fa[v.back()] != 0) return deal(v.back()), void();
fa[v.back()] = 0;
}
puts("NO");
}
T3 - 如何正确地排序
人间之屑😊
考虑求 \(\max\) 的部分,\(\min\) 只要令 \(a_{i,j}\gets-a_{i,j}\)(其中 \(a_{i,j}\) 表示向量 \(\pmb a_i\) 的第 \(j\) 分量)再做一次,答案取相反数。
考虑 \(a_{i,x}\) 对答案的贡献系数。其实就是满足对所有 \(y\neq x\) 都满足 \(a_{i,x}+a_{j,x}\ ?\ a_{i,y}+a_{j,y}\)(其中 \(?\) 当 \(y<x\) 时为 \(>\),否则为 \(\geq\))的 \(j\) 的数量。\(a_{i,x}+a_{j,x}\leq a_{i,y}+a_{j,y}\),即 \(a_{i,x}-a_{i,y}\leq a_{j,y}-a_{j,x}\),那就是个静态 \(m-1\) 维数点。。。。。。。。。。。直接 cdq + BIT 硬做就行了,当 \(m=4\) 时是 2log。
这么大一个数据结构题 vp 的时候竟然一遍过样例了/jy?可能是因为 cdq 比较层次分明罢。
略微卡常,本地 3.3s,交上去只跑了 2.7s😄
code
constexpr int N = 4e5 + 10;
int m, n;
int a[N][5];
int t[N];
struct query3 {
int a, b, c, i;
query3(int a = 0, int b = 0, int c = 0, int i = 0) : a(a), b(b), c(c), i(i) {}
bool operator<(const query3 &q) const { return tie(a, i) < tie(q.a, q.i); }
} q3[N];
struct query2 {
int a, b, i;
query2(int a = 0, int b = 0, int i = 0) : a(a), b(b), i(i) {}
query2(const query3 &q) : a(q.b), b(q.c), i(q.i) {}
bool operator<(const query2 &q) const { return tie(a, i) < tie(q.a, q.i); }
} q2[N];
struct query1 {
int a, i;
query1(int a = 0, int i = 0) : a(a), i(i) {}
query1(const query2 &q) : a(q.b), i(q.i) {}
bool operator<(const query1 &q) const { return tie(a, i) < tie(q.a, q.i); }
} q1[N];
struct bitree {
int cnt[2 * N];
void add(int x, int v) {
x += N + 10 >> 1;
static constexpr int lim = 1.5 * (N + 10);
while(x <= lim) cnt[x] += v, x += lowbit(x);
}
int Cnt(int x) {
x += N + 10 >> 1;
int ans = 0;
while(x) ans += cnt[x], x -= lowbit(x);
return ans;
}
} bit;
void d1(query1*, int, int);
void s1(query1 *q, int l, int r) { d1(q, l, r); }
void d1(query1 *q, int l, int r) {
REP(i, l, r) {
int v = q[i].a, id = q[i].i;
if(id) t[id] += bit.Cnt(v);
else bit.add(v, 1);
}
REP(i, l, r) {
int v = q[i].a, id = q[i].i;
if(!id) bit.add(v, -1);
}
}
void s2(query2 *q, int l, int r, bool _sorted = false) {
if(!_sorted) stable_sort(q + l, q + r + 1);
static query1 p[N];
REP(i, l, r) p[i] = q[i];
d1(p, l, r);
}
void d2(query2 *q, int l, int r) {
if(l == r) return;
int mid = l + r >> 1;
d2(q, l, mid), d2(q, mid + 1, r);
int n = 0, m = 0; static query2 p[N];
REP(i, l, mid) if(!q[i].i) p[++n] = q[i];
REP(i, mid + 1, r) if(q[i].i) p[++m + n] = q[i];
inplace_merge(p + 1, p + n + 1, p + n + m + 1);
s2(p, 1, n + m, true);
inplace_merge(q + l, q + mid + 1, q + r + 1);
}
void s3(query3 *q, int l, int r) {
stable_sort(q + l, q + r + 1);
static query2 p[N];
REP(i, l, r) p[i] = q[i];
d2(p, l, r);
}
ll solve() {
ll ans = 0;
REP(d, 1, m) {
memset(t, 0, sizeof(t));
REP(j, 1, n) {
vi v;
REP(e, 1, m) if(d != e) v.pb(a[j][e] - a[j][d]);
if(m == 2) q1[j] = query1(v[0], 0);
else if(m == 3) q2[j] = query2(v[0], v[1], 0);
else q3[j] = query3(v[0], v[1], v[2], 0);
}
REP(i, 1, n) {
vi v;
REP(e, 1, m) if(d != e) v.pb(a[i][d] - a[i][e] - (d < e));
if(m == 2) q1[i + n] = query1(v[0], i);
else if(m == 3) q2[i + n] = query2(v[0], v[1], i);
else q3[i + n] = query3(v[0], v[1], v[2], i);
}
if(m == 2) s1(q1, 1, 2 * n);
else if(m == 3) s2(q2, 1, 2 * n);
else s3(q3, 1, 2 * n);
REP(i, 1, n) ans += (ll)t[i] * a[i][d];
}
return ans;
}
void mian() {
m = read(), n = read();
REP(i, 1, m) REP(j, 1, n) a[j][i] = read();
ll ans = solve();
REP(i, 1, n) REP(j, 1, m) a[i][j] *= -1;
ans -= solve();
prt(2 * ans), pc('\n');
}
pj 组
T1 - 王国比赛
直接模拟即可😁
T2 - 数学游戏
《暴力破解游戏》
先令 \(z\gets\dfrac{z}x\)(如果不整除就 exit)。然后就是 \(y\gcd(x,y)=z\)。
对每个质因子分别考虑,设 \(x\) 含 \(a\) 个该质因子,\(y\) 含 \(X\) 个,\(z\) 含 \(Y\) 个,那就是 \(X+\min(a,X)=Y\)。LHS 显然随 \(X\) 严格单调递增,所以解是唯一的,他奶奶的(
如果 \(2a<Y\),那么 \(X=Y-a\);如果 \(2a\geq Y\),那么 \(X=\dfrac{Y}2\),前提是 \(2\mid Y\),需要 chk 一下。但我们显然不能真的分解质因数,于是考虑基于数本身的整体性方法。
考虑 \(x^2\),这样质因子有 \(b=2a\) 个。先考虑 chk,如果 \(b<Y\) 那没事,如果 \(b\geq Y\) 则需要 \(Y\) 是偶数。注意到 \(b\) 一定是偶数,那么把「那没事」换成 \(b\) 是偶数也是可行的,也就是 \(\min(b,Y)\) 必须是偶数。这也就是 \(e=\gcd(x^2,z)\) 要是完全平方数(这里我是二分求平方根的,害怕 ll
遇 sqrt
炸精度),设其平方根为 \(d\)。
接下来求 \(y\)。如果 \(2a\geq Y\) 可以变形成 \(X=Y-\dfrac{Y}2\),于是无论如何 \(X=Y-\min\!\left(a,\dfrac Y2\right)\),而减号后面恰好对应 \(d\)。所以 \(y=\dfrac{z}d\)。
code
void mian() {
int x = read(), z = read();
if(z % x) return puts("-1"), void();
z /= x;
int d = gcd(x * x, z);
int rt = 0;
PER(i, 32, 0) {
int nw = rt + (1ll << i);
if(nw <= d / nw) rt = nw;
}
if(rt * rt != d) return puts("-1"), void();
int y = z / rt;
prt(y), pc('\n');
}
T3 - 字符串
考虑 DP。
考察最终的状态,也就是 \(a_{1\sim n}\) 要造出 \(b_{1\sim m}\)。如果 \(a_n\neq\texttt -\),那么直接判断是否有 \(a_n=b_m\) 并令 \(n\gets n-1,m\gets m-1\),将问题规模成功缩小。
如果 \(a_n=\texttt-\),考虑决策这个删的是前面还是后面,如果是前面,那么归约到 \(a_{1\sim n-1}\) 造出 \(\texttt?+b_{1\sim m}\) 的状态(其中 \(\texttt?\) 表示通配符),如果是后面就是 \(b_{1\sim m}+\texttt?\),把两者加起来即可。现在引入了新的状态形式:\(a_{1\sim i}\) 造出 \((\texttt?)+b_{1\sim j}+(\texttt ?)\)。容易预见到,还会扩展至形如 \(a_{1\sim i}\) 造出 \(k\times\texttt?+b_{1\sim j}+o\times\texttt?\) 的状态。
现在转移封闭了。如果 \(a_i=\texttt-\) 那就直接匹配呗(有可能跟通配符匹配的哟),否则就往 k += 1
和 o += 1
转移。这样看上去是四方的,实际上通过 \(i,k,o\) 显然可以确定 \(j\),所以是三方的。
就……做完了?/qd
code
constexpr int N = 410;
int n, m;
char a[N], b[N];
int cnt[N];
int dp[N][N][N];
void mian() {
n = read(), m = read();
reads(a + 1), reads(b + 1);
REP(i, 1, n) cnt[i] = cnt[i - 1] + (a[i] == '-' ? -1 : 1);
if(cnt[n] != m) return puts("0"), void();
memset(dp, 0, sizeof(dp));
dp[0][0][0] = 1;
REP(i, 1, n) REP(l, 0, cnt[i]) REP(r, 0, cnt[i] - l) {
int mid = cnt[i] - l - r;
if(a[i] == '-') {
dp[i][l][r] = add(dp[i - 1][l + 1][r], dp[i - 1][l][r + 1]);
} else {
if(r) dp[i][l][r] = dp[i - 1][l][r - 1];
else if(mid) {
if(a[i] == b[mid]) dp[i][l][r] = dp[i - 1][l][r];
} else dp[i][l][r] = dp[i - 1][l - 1][r];
}
}
prt(dp[n][0][0]), pc('\n');
}