Loading

NOIP2023 题解

前言

大家好,还有 \(5\) 天就 NOIP2024 了,今天来写一下 NOIP2023 的题解。

T1

主要考察了字符串的排序性质,给字符串从大到小排序会使它字典序最大,从小到大排序会使它字典序最小。
那么只要给所有字符串分别从大到小,从小到大排序,枚举每个字符串,看看这个字符串字典序最小的时候是否比别的字符串字典序最大的时候字典序小。
时间复杂度 \(\mathcal{O}(n^2)\),当然你想要写线段树维护从大到小排序排序的字符串的字典序最小值优化到 \(\mathcal{O}(n \log n)\) 也没人拦着。

Code:

int n, m;
string s[N], s1[N], s2[N];
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        s1[i] = s2[i] = s[i];
        sort(s1[i].begin(), s1[i].end());
        sort(s2[i].begin(), s2[i].end());
        reverse(s2[i].begin(), s2[i].end());
    }
    for (int i = 1; i <= n; i++) {
        bool ok = 1;
        for (int j = 1; j <= n; j++) {
            if (s2[j] < s1[i]) {
                ok = 0;
                break;
            }
        }
        cout << (ok ? 1 : 0);
    }
    cout << '\n';
    return 0;
}

T2

我认为是一道好题。

以下内容参考了 @Svemit 的题解

首先题目可以抽象成图论问题,我们记 \(\lnot x\)\(-x\),但是因为实际上不能用负下标,我们在实现中用 \(n + x\) 代替(类似分层图),另外,我们让 \(T,F,U\) 分别代表一些常量,由于 \(-U = U\),故令 \(U = 0\)\(T,F\) 不重要,互为相反数且不为 \(0\) 即可。

则对于每个数 \(x\),有两种状态:

1.\(x\) 最终被赋值了,记值为 \(val_x\).

2.\(x\) 指向某个数,即 \(x\) 会随某个数的改变而改变,我们记这个数为 \(p_x\),但是 \(x\) 也可能为 \(-p_x\),所以我们要记一个 \(sgn_x\) 表示 \(x = sgn_x \times p_x\),即 \(x\) 最终为 \(p_x\) 还是 \(-p_x\). 当然既然是图论问题,也存在 \(x\) 最终指向自己的问题,这时出现了一个环,环上所有点都是相同的,\(x\) 是什么它们就是什么.

上述信息很明显可以并查集维护。

接下来我们来看看什么情况会出现必须选 \(U\) 的情况,同样是两种:

1.\(val_x = U\),那么 \(x\) 和所有指向 \(x\) 的点必须是 \(U\).

2.并查集中 \(x\)\(-x\) 所在集合相同,即 \(x\)\(-x\) 指向相同,那么这个集合也得是 \(U\).

知道了这些,就可以算答案了.

时间复杂度 \(\mathcal{O}(Tn\alpha(n))\)

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, mod = 998244353, inf = 1 << 30, T = 1, F = -1, U = 0;
typedef long long ll;
typedef pair<int, int> pii;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; } 
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
	#ifdef ONLINE_JUDGE
		return ;
	#endif
	cerr << arg << ' ';
	dbg(args...);
}   
int c, Tc, n, m, fa[N], sz[N], val[N], p[N], sgn[N], ans; //x 最终会随着 p[x] 的改变而改变, val[x] = sgn[x] * val[p[x]]
map<char, int>mp = {{'T', T}, {'F', F}, {'U', U}};
int find(int pos) { return pos == fa[pos] ? pos : fa[pos] = find(fa[pos]); }
void merge(int x, int y) {
	x = find(x), y = find(y);
	if (x == y) return ;
	fa[x] = y, sz[y] += sz[x];
}
void update(int x) {
	x = find(x);
	ans += sz[x];
	sz[x] = 0;
}
int main() {
	ios::sync_with_stdio(false); cin.tie(nullptr);
	cin >> c >> Tc;
	while (Tc--) {
		ans = 0;
		cin >> n >> m;
		for (int i = 1; i <= n; i++) p[i] = i, sgn[i] = 1, val[i] = inf;
		for (int i = 1; i <= (n << 1); i++) fa[i] = i, sz[i] = (i <= n);
		while (m--) {
			char op;
			int a, b;
			cin >> op;
			if (op == '+' || op == '-') {
				cin >> a >> b;
				if (op == '+') {
					if (val[b] != inf) {
						val[a] = val[b];
					} else {
						val[a] = inf;
						p[a] = p[b];
						sgn[a] = sgn[b];
					}
				} else {
					if (val[b] != inf) {
						val[a] = -val[b];
					} else {
						val[a] = inf;
						p[a] = p[b];
						sgn[a] = -sgn[b];
					}
				}
			} else {
				cin >> a;
				val[a] = mp[op];
			}
		}
		for (int i = 1; i <= n; i++) if (val[i] == inf) {
			int x = p[i];
			if (sgn[i] == 1) {
				merge(i, x), merge(i + n, x + n);
			} else {
				merge(i + n, x), merge(i, x + n);
			}
		} //并查集
		for (int i = 1; i <= n; i++) {
			if (val[i] == U) {
				update(i), update(i + n);
			} else if (find(i) == find(i + n)) {
				update(i);
			}
		} // 算答案(注意算完一个后要清空,因为一个集合可能有多个 x 和 -x 同时存在,但只能算一次)
		cout << ans << '\n';
	}
	return 0;
}

T3

很巧妙的题。

以下内容参考了 @liangbowen 的题解。

首先明白题目的限制条件就是 \(\forall 1 \le i \ge 10^{100} f_i > g_i\)\(\forall 1 \le i \ge 10^{100} f_i < g_i\),两边只要判断一边即可,另一边只需要 \(x, y、n, m\) 互换再判断即可.
我们来考虑前者,还是可以转化成图论问题思考:

如图,横坐标表示当前匹配到的是 \(X_i\),纵坐标表示当前匹配的是 \(Y_i\).
题目相当于是说我们当前匹配到了 \((1, 1)\)(若 \(X_1 \ge Y_1\) 就直接倒闭了),要走到 \((n, m)\)(即匹配完所有),是否可行,每次匹配,如果匹配到了 \((i, j)\),若 \(X_i<Y_{i+1}\),则可以走到 \((i,j+1)\)\(X_{i+1}<Y_i\),则可以走到 \((i+1,j)\)\(X_{i+1}<Y_{i+1}\),则可以走到 \((i+1,j+1)\).形式化地,记 \(A_{i, j}=[X_i < Y_i]\),若走到了 \((i, j)\),且 \(A_{i+0/1,j+0/1} = 1\),则可以走到 \((i+0/1,j+0/1)\).
直接暴力走是 \(\mathcal{O}(nmq)\) 的,接下来考虑优化.

看到特殊性质产生联想,容易发现如果 \(X_{min} \le Y_{min}\),则 \(Y_{min}\) 这行都是 \(0\),是无解的.同理如果 \(X_{max} \ge Y_{max}\),那么 \(X_{max}\) 这列都是 \(0\),也无解.

如果过了以上特判,那么特殊性质的图就有了一个强大的性质:

\(\forall 1 \le i \ge m, A_{n,i} = 1\)
\(\forall 1 \le i \ge n, A_{i, m} = 1\)

那么图上就多了两条线,线上的点都是 \(1\).如图所示

此时由于我们的目的是走到 \((n,m)\),而走到线上任意一个点都可以走到 \((n,m)\),所以我们只要走到线上即可,又有走到第 \(n-1\) 列或 第 \(m-1\) 行就能走到线上,所以题目的要求缩小成了走到第 \(n-1\) 列或 第 \(m-1\) 行。

由特殊性质的启发,我们想到去寻找这两条线,然后判断是否能从 \((1, 1)\) 走到线上,并从线上走到 \((n, m)\) 即可,那么就是递归求解,每次寻找前缀/后缀最大值的位置就行了,并且这样肯定是更优的。

时间复杂度 \(\mathcal{O}((n+m)q)\).

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10, mod = 998244353;
typedef long long ll;
typedef pair<int, int> pii;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; } 
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
	#ifdef ONLINE_JUDGE
		return ;
	#endif
	cerr << arg << ' ';
	dbg(args...);
}   
// 由题目, 得到条件为 \forall 1 <= i <= 1e100 f[i] < g[i] 或 \forall 1 <= i <= 1e100 f[i] > g[i], 由对称性, 考虑一边即可  
// 转化成图论问题, 设 A[i][j] = [X[i] < Y[j]], 则相当于从 (1, 1) 开始, 每次往下拓展一次, e.g. 若可以走到 (i, j) 且 A[i + 1][j] = 1, 则可以走到 (i + 1, j). 问是否能走到 (n, m).
// 如果 x_min >= y_min, 则 y_min 这行都是 0, 不行, 同理如果 x_max >= y_max, 则 x_max 这列都是 0, 也不行
// 若过了这两个特判, 则只要能从 (1, 1) 走到 x_min 那列或 y_max 那行, 并能从 x_min 那列或 y_max 那行走到 (n, m) 就行了
// 找到最大最小值后,可以递归缩小范围, 详细见代码实现
// 时间复杂度 O((n + m)q)
struct Info {
	int mx, mn; //这里存的都是位置
} preX[N], sufX[N], preY[N], sufY[N];
void update(Info &A, int T[], const int &i, const Info &p) {
	A = {T[i] > T[p.mx] ? i : p.mx, T[i] < T[p.mn] ? i : p.mn};
}
int x[N], y[N], e[N], f[N], a[N], b[N];
bool check1(int x, int y) { //(1, 1) -> (x, y)
	if (x == 1 || y == 1) return true;
	Info X = preX[x - 1], Y = preY[y - 1];
	if (a[X.mn] < b[Y.mn]) return check1(X.mn, y);
	if (a[X.mx] < b[Y.mx]) return check1(x, Y.mx); //这里是在找新的红线,新红线和两条旧红线必有交点所以互相可达.
	return false;
}
bool check2(int x, int y, int n, int m) { //(x, y) -> (n, m)
	if (x == n || y == m) return true;
	Info X = sufX[x + 1], Y = sufY[y + 1];
	if (a[X.mn] < b[Y.mn]) return check2(X.mn, y, n, m);
	if (a[X.mx] < b[Y.mx]) return check2(x, Y.mx, n, m);
//同理
	return false;
}
bool solve(int tx[], int ty[], int n, int m) { //能否 f[i] < g[i]
	if (tx[1] >= ty[1]) return false;
	for (int i = 1; i <= n; i++) a[i] = tx[i];
	for (int i = 1; i <= m; i++) b[i] = ty[i];
	preX[1] = preY[1] = {1, 1}, sufX[n] = {n, n}, sufY[m] = {m, m};
	for (int i = 2; i <= n; i++) {
		update(preX[i], a, i, preX[i - 1]);
	}
	for (int i = 2; i <= m; i++) {
		update(preY[i], b, i, preY[i - 1]);
	}
	for (int i = n - 1; i; i--) {
		update(sufX[i], a, i, sufX[i + 1]);
	}
	for (int i = m - 1; i; i--) {
		update(sufY[i], b, i, sufY[i + 1]);
	}
	Info X = preX[n], Y = preY[m];
	if (a[X.mn] >= b[Y.mn] || a[X.mx] >= b[Y.mx]) return false;
	return check1(X.mn, Y.mx) && check2(X.mn, Y.mx, n, m);
}
int main() {
	int c, n, m, Q;
	scanf("%d%d%d%d", &c, &n, &m, &Q);
	for (int i = 1; i <= n; i++) scanf("%d", &x[i]);
	for (int i = 1; i <= m; i++) scanf("%d", &y[i]);
	putchar((solve(x, y, n, m) || solve(y, x, m, n)) ? '1' : '0');
	while (Q--) {
		for (int i = 1; i <= n; i++) e[i] = x[i];
		for (int i = 1; i <= m; i++) f[i] = y[i];
		int kx, ky;
		scanf("%d%d", &kx, &ky);
		while (kx--) {
			int p, v;
			scanf("%d%d", &p, &v);
			e[p] = v;
		}
		while (ky--) {
			int p, v;
			scanf("%d%d", &p, &v);
			f[p] = v;
		}
		putchar((solve(e, f, n, m) || solve(f, e, m, n)) ? '1' : '0');
	}
	return 0;
}

T4

不是很难的题.下面讲讲我的思考过程.

首先我看到 \(m\) 很小,类比到之前一道 AT 的题,\(dp_i\) 表示考虑到前 \(i\) 个挑战的最大值,然后推式子,开两颗线段树维护,但是这样似乎无法维护区间的包含关系,即只能做特殊性质 BC.

注:@WrongAnswer_90 说其实是可以过的,但是实现比较麻烦,具体可以看this(/bx/bx)

认为这个假了之后,我就不会了...看了题解后才发现原来可以把状态设计成 \(f_{i,0}\) 表示前 \(i\) 天,第 \(i\) 天不跑的最大收益,\(f_{i,1}\) 表示前 \(i\) 天第 \(i\) 天跑的收益,显然

\[f_{i,0}=\max\limits_{j=1}^i f_{j,1} \]

\[f_{i,1}=\max\limits_{j=i-k}^{i-1} f_{i,0}-(i-j)\times d+calc(j,i) \]

其中 \(calc(x, y)\) 表示 \(x + 1\)\(y\) 连续跑能完成的挑战获得的能量值之和。这个可以双指针完成,动态地给 \([0,l_p-1]\) 区间加上 \(v_p\).另外 \(-(i-j) \times d\) 也可以动态,每次区间 \(-d\) 即可。

当前时间复杂度为 \(\mathcal{O}(n \log n)\),考虑优化,发现决策点(\(i\)\(j\))只会为 \(l_i-1\)\(r_i\),离散化即可,但是上面的动态修改要稍作改动。

时间复杂度 \(\mathcal{O}(m \log m)\)

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, mod = 998244353;
const long long inf = -(1ll << 60);
typedef long long ll;
typedef pair<int, int> pii;
void cmax(ll &x, ll y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; } 
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
	#ifdef ONLINE_JUDGE
		return ;
	#endif
	cerr << arg << ' ';
	dbg(args...);
}   
int C, T, n, m, k, d, c[N << 1], tot;
struct Event {
	int l, r, v;
	bool operator < (const Event &A) const {
		return r < A.r;
	}
} a[N];
struct SegTree {
	#define ls(x) (x << 1)
	#define rs(x) ((x << 1) | 1)
	ll tree[N << 3], tag[N << 3]; //N << 1 << 2 
	void pushup(int pos) { tree[pos] = max(tree[ls(pos)], tree[rs(pos)]); }
	void pushdown(int pos) {
		if (tag[pos]) {
			ll &t = tag[pos];
			tree[ls(pos)] += t, tree[rs(pos)] += t;
			tag[ls(pos)] += t, tag[rs(pos)] += t;
			t = 0;
		}
	}
	void build(int l = 0, int r = tot - 1, int pos = 1) {
		tree[pos] = 0ll, tag[pos] = 0ll;
		if (l == r) {
			return ;
		}
		int mid = (l + r) >> 1;
		build(l, mid, ls(pos)); build(mid + 1, r, rs(pos));
		pushup(pos);
	}
	void modify(int x, int y, ll v, int l = 0, int r = tot - 1, int pos = 1) {
		if (x <= l && r <= y) {
			tree[pos] += v;
			tag[pos] += v;
			return ;
		}
		pushdown(pos);
		int mid = (l + r) >> 1;
		if (x <= mid) modify(x, y, v, l, mid, ls(pos));
		if (mid < y) modify(x, y, v, mid + 1, r, rs(pos));
		pushup(pos);
	}
	ll query(int x, int y, int l = 0, int r = tot - 1, int pos = 1) {
		if (x <= l && r <= y) return tree[pos];
		int mid = (l + r) >> 1;
		pushdown(pos);
		ll ans = 0;
		if (x <= mid) ans = max(ans, query(x, y, l, mid, ls(pos)));
		if (mid < y) ans = max(ans, query(x, y, mid + 1, r, rs(pos)));
		return ans; 
	}
} st;
int main() {
	scanf("%d%d", &C, &T);
	while (T--) {
		tot = 0;
		scanf("%d%d%d%d", &n, &m, &k, &d);
		for (int i = 1; i <= m; i++) {
			scanf("%d%d%d", &a[i].r, &a[i].l, &a[i].v);
			a[i].l = a[i].r - a[i].l + 1 - 1;
			c[++tot] = a[i].l, c[++tot] = a[i].r;
		}
		sort(c + 1, c + tot + 1);
		tot = unique(c + 1, c + tot + 1) - (c + 1);
		st.build();
		for (int i = 1; i <= m; i++) {
			a[i].l = lower_bound(c + 1, c + tot + 1, a[i].l) - c;
			a[i].r = lower_bound(c + 1, c + tot + 1, a[i].r) - c;
		}
		sort(a + 1, a + m + 1);
		ll ans = 0;
		for (int i = 1, j = 1; i <= tot; i++) {
			st.modify(0, i - 1, -1ll * (c[i] - c[i - 1]) * d);
			while (j <= m && a[j].r == i) {
				st.modify(0, a[j].l, a[j].v), j++;
			}
			cmax(ans, st.query(lower_bound(c + 1, c + tot + 1, c[i] - k) - c, i - 1));
			if (i + 1 < tot) st.modify(i + 1, i + 1, ans);
		}	
		printf("%lld\n", ans);
	}
	return 0;
}
posted @ 2025-12-17 15:45  循环一号  阅读(4)  评论(0)    收藏  举报