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)\) 要是完全平方数(这里我是二分求平方根的,害怕 llsqrt 炸精度),设其平方根为 \(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 += 1o += 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');
}
posted @ 2022-04-03 23:02  ycx060617  阅读(221)  评论(1编辑  收藏  举报