11.7 NOIP模拟赛 题解

T1 Odd Subarrays


原题链接

来将题意简单转化一下:

1 ∼ n 1∼n 1n的一个排列1分划成若干连续子序列,使得逆序对对数为奇数的子序列数量最多。

我们考虑其中一个数 a i a_i ai以及一个最小的满足 a j < a i a_j<a_i aj<ai j , j, j现在考虑区间 ( i , j ) 。 (i,j)。 (i,j)显然,对于任意的 i < k < j i<k<j i<k<j必有 ( a k , a j ) (a_k,a_j) (ak,aj)是一对逆序对。此时,将该区间进一步划分为 ( i , j − 2 ) (i,j-2) (i,j2) ( j − 1 , j ) (j-1,j) (j1,j),则 ( j − 1 , j ) (j-1,j) (j1,j)必然是一个逆序对数为奇数的子序列,即这样划分两个区间后,逆序对对数为奇数的子序列数一定比之前划分的多。因此,我们最终只需要统计满足 a i ? a i + 1 a_i?a_{i+1} ai?ai+1的配对个数即可。

c o d e code code

#include<bits/stdc++.h>
using namespace std;
int t,n,p[100005],ans;
int main(){
	cin>>t;
	while(t--){
		cin>>n;
		ans=0;
		for(int i=0;i<n;i++)cin>>p[i];
		for(int i=0;i<n-1;i++){
			if(p[i]>p[i+1]){
				ans++;
				i++;//防止重复配对
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

记不清楚顺序了,就一道一道来了。

AT_arc174_f [ARC174F] Final Stage


原题链接

这道是黑题啊,畏惧了。但也就是dp+堆

还是一样的步骤,先来简化一下题意:

有一堆石子,两个人轮流取石子,共取 n n n次,第 i i i次要取 [ l i , r i ] [l_i,r_i] [li,ri]个石子,无法操作的人输, q q q次询问如果初始有 c c c个石子,谁会获胜。

来想想思路:

如果 n = 1 n=1 n=1,那么 [ 0 , l i ) [0,l_i) [0,li)必败, [ l i , + ∞ ) [l_i,+\infty ) [li,+)平局。

然后考虑加入 [ l n − 1 , r n − 1 ] [l_{n-1},r_{n-1}] [ln1,rn1],此时对于每个必败区间 [ x , y ] [x,y] [x,y],其都会变成 [ x + l n − 1 , y + r n − 1 ] [x+l_{n-1},y+r_{n-1}] [x+ln1,y+rn1]并取并,然后翻转必胜必败情况,最后设 [ 0 , 0 ] [0,0] [0,0]必败。

我们要动态维护这些操作,可以只维护所有差分位置,即 c = i − 1 c=i-1 c=i1 c = i c=i c=i时结果不同的位置。

那么奇数项差分位置就是必败区间的左端点,偶数项差分位置就是必败区间的右端点。

给不同奇偶的差分位置分别加上 l n − 1 / r n − 1 l_{n-1}/r_{n-1} ln1/rn1,如果两个差分位置相交,说明两个必败区间相交,把这两个差分位置都删除。

用堆维护所有奇偶差分位置的距离,每次把会相遇的差分位置弹出并暴力删除即可,双向链表维护所有差分位置即可。

c o d e code code

#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
using namespace std;
ll read() {
	ll x = 0, f = 1;
	char ch = getchar();
	while (ch > '9' || ch < '0') {
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}
const int MAXN = 3e5 + 5;
ll l[MAXN], r[MAXN], x[MAXN];
ll tl, tr;
ll ans[MAXN];
int n, q;
int lr[MAXN], ls[MAXN];
set <pair<ll, int>> ql, qr;
signed main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) {
		l[i] = read(), r[i] = read();
	}
	int hd = 1, tot = 2;
	ls[1] = 2, lr[2] = 1, x[2] = l[n];
	ql.insert({l[n], 1});
	for (int k = n - 1; k; --k) {
		tl += l[k], tr += r[k];
		while (qr.size() && qr.begin()->fi <= tr - tl) {
			int i = qr.begin()->second, j = ls[i];
			qr.erase(qr.begin());
			if (!ls[j]) {
				ls[i] = 0;
				continue;
			}
			if (!lr[i]){
				hd = ls[j];
			}
			ls[lr[i]] = ls[j];
			lr[ls[j]] = lr[i];
			if (lr[i]){
				ql.erase({x[i] - x[lr[i]], lr[i]});
			}
			if (ls[j]){
				ql.erase({x[ls[j]] - x[j], j});
			}
			if (lr[i] && ls[j]){
				ql.insert({x[ls[j]] - x[lr[i]], lr[i]});
			}
		}
		swap(tl, tr);
		swap(ql, qr);
		++tot, lr[hd] = tot, ls[tot] = hd;
		x[tot] = -tl, ql.insert({x[hd] - x[tot], tot}), hd = tot;
	}
	int m = 0;
	for (int i = hd; i; i = ls[i]) {
		++m;
		if (m & 1) ans[m] = x[i] + tl;
		else ans[m] = x[i] + tr;
	}
	scanf("%d",&q);
	while (q--) {
		ll z;
		z = read();
		int i = upper_bound(ans + 1, ans + m + 1, z) - ans - 1;
		if (i == m) puts("Draw");
		else {
			if (i & 1) puts("Bob");
			else puts("Alice");
		}
	}
	return 0;
}

T3 Range Sorting (Hard Version)


原题链接

这道题还行,在luogu交了RE了4个点,在原题交没事。

一样的思路,简化题意:

给定长度为n的数组a,n个元素两两不同。 1 ≤ n ≤ 3 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 9 1\le n\le{3*10^5},1\le{a_i}\le{10^9} 1n3105,1ai109

定义一个数组 p 1 , p 2 , . . . , p k p1,p2,...,pk p1,p2,...,pk的美丽值为,将它升序排序所需要的最小开销。 每次操作,可以选择一个区间, [ l , r ] [l,r] [l,r]并将数组的区间 [ l , r ] [l,r] [l,r]进行升序排序。对应端开销为 [ r − l ] [r-l] [rl]

给定数组a,求它所有连续子数组的美丽值之和。

定义数组a的连续子数组为,选择下标 1 ≤ l ≤ r ≤ n 1\le{l}\le{r}\le{n} 1lrn后,截取的子数组 a [ l ] , a [ l + 1 ] , . . . , a [ r ] 。 a[l],a[l+1],...,a[r]。 a[l],a[l+1],...,a[r]

考虑极端情况,整个数组都是逆序的。
n , n − 1 , n − 2 , . . . , 2 , 1 n, n-1, n-2, ... , 2, 1 n,n1,n2,...,2,1
那么这时候,所有连续子数组的美丽值之和,就为

res = 0;
for i = 1 to n:
    tmp = 0 + 1 + 2 + ... + i-1 = i * (i - 1) / 2;
    res += tmp;

我们可以先假设所有子区间需要翻转。再减去实际上为正序的区间。 我们可以枚举每个下标位置,分别求出小于 a [ i ] a[i] a[i]的左区间,以及大于 a [ i ] a[i] a[i]的右区间。这些即为正序的区间。

c o d e code code

//stl+dp
//https://codeforces.com/problemset/problem/1827/B2
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (ch > '9' || ch < '0') {
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}
void write(int x) {
	if (x < 0) putchar('-'), x = -x;
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
const int N = 500005;
const int M = 19;//区间长度
int a[N], r[N];
int f[M][N];
//f[k][i]表示 以i为右边界,i - (1<<k) + 1为 左边界的 区间最大值
void solve() {
	int n;
	n = read();
	int res = 0;
	vector<int> s;//stl大法好
	a[0] = a[n + 1] = 0;
	for (int i = 1; i <= n; i++) {
		a[i] = read();
		f[0][i] = a[i];//头尾都初始化为0
		// 假设所有区间都需要翻转,计算所有所有的区间和
		res += 1ll * i * (i - 1) / 2;
	}//右区间入队
	s.push_back(n + 1);
	for (int i = n; i > 0; i--) {
		while (a[s.back()] > a[i]) {// 单调栈,用于求r[i]
			s.pop_back();
		}
		r[i] = s.back(); // r[i]记录 从下标i开始,不小于i的元素的 右边界
		s.push_back(i);
	}
	for (int i = 1; i < M; i++) {// f算法求区间,最大值
		for (int j = (1 << i); j <= n; j++) {
			f[i][j] = max(f[i - 1][j], f[i - 1][j - (1 << (i - 1))]);
		}
	}
	s.clear();
	s.push_back(0);
	for (int i = 1; i <= n; i++) {
		while (a[s.back()] > a[i]) {// 单调栈,用于求s.back()
			s.pop_back();
		}
		// s.back() 表示  从下标i开始,小于i的元素的起始右边界
		// j表示 从下标s.back() 开始, [j,s.back()]区间最大值 < a[i]的最左边界
		int j = s.back();
		for (int k = M - 1; k >= 0; k--) {
			if (j >= (1 << k) && f[k][j] < a[i]) {
				j -= 1 << k;
			}
		}
		//记得long long
		res -= 1ll * (r[i] - i) * (s.back() - j); // 减去不需要翻转的区间
		s.push_back(i);
	}
	write(res);
	cout << endl;
}
signed main() {
	int t;
	cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

T4 AT_abc337_g [ABC337G] Tree Inversion


原题链接

蒟蒻(ju ruo)

逆天,这道题竟然用到了换根dp知识点,没事,继续学!

下面我们来了解换根dp的一些基础知识点:
换根dp一般可以将 O ( n 2 ) O(n^2) O(n2)的做法优化到线性 O ( n ) O(n) O(n)
换根dp分为3个步骤:

1. 1. 1. 先指定一个根节点。
2. 2. 2. 一次dfs统计子树内的节点对当前节点的贡献。
3. 3. 3. 一次dfs统计父亲节点 对当前节点的贡献并合并统计最终答案。

下面来步入正题:

既然知道了是换根dp,那我们如果用朴素的做法求解,时间复杂度就是 O ( n 2 ) O(n^2) O(n2)

下面来讲一下换根dp的思路:
这个题发现就是求类似树上逆序对的个数,即在同一个子树里,节点编号和深度大小关系相反。考虑第一步,设 f [ x ] f[x] f[x]表示以节点编号 x x x为根的子树包含逆序对的个数。显然,我们要知道子树中编号小于 x x x的节点数量 a x a_x ax,那么状态转移方程就很显然了:

f [ x ] = a x + ∑ y ∈ s o n ( x ) f [ y ] f[x]=a_x+\sum_{y\in{son(x)}}f[y] f[x]=ax+yson(x)f[y]

接下来考虑第二部,设 g [ x ] g[x] g[x]表示以 x x x为根节点的树的逆序对的个数,思考由 x x x换根为 y y y后, g [ x ] g[x] g[x]通过什么样的转换关系到达 g [ y ] g[y] g[y],通过下面的图示来清楚看懂。

在这里插入图片描述
这里还需要添加一个 b x b_x bx表示以 x x x为根的子树内编号小于 f a [ x ] fa[x] fa[x]的节点数量。
特别注:总的小于 y y y的节点个数是 y − 1 y-1 y1,已知 y y y子树内的数量有 a y a_y ay,相减即子树外的部分 g [ y ] = g [ x ] − b y + ( y − 1 − a y ) g[y]=g[x]-b_y+(y-1-a_y) g[y]=g[x]by+(y1ay)

下面求解 a x , a y a_x,a_y ax,ay,可以用树状数组,作差一下,用全局的值减掉子树外的值即可。

c o d e code code

#include <bits/stdc++.h>
#define re register int
#define lp p << 1
#define rp p << 1 | 1
#define int long long
using namespace std;
const int N = 2e5 + 10;
struct Edge {
	int to, next;
} e[N << 1];
int idx, h[N];
struct Tree {
	int l, r, sum;
} t[N << 2];
int n, f[N], g[N], a[N], b[N];
inline void add(int x, int y) {
	e[++ idx] = (Edge) {
		y, h[x]
	};
	h[x] = idx;
}
inline void build(int p, int l, int r) {
	t[p].l = l, t[p].r = r;
	if (l == r) return;
	int mid = (l + r) >> 1;
	build(lp, l, mid);
	build(rp, mid + 1, r);
}
inline void update(int p, int x, int k) {
	if (t[p].l == x && t[p].r == x) {
		t[p].sum += k;
		return;
	}
	int mid = (t[p].l + t[p].r) >> 1;
	if (x <= mid) update(lp, x, k);
	if (x > mid) update(rp, x, k);
	t[p].sum = t[lp].sum + t[rp].sum;
}
inline int query(int p, int l, int r) {
	if (l > r) return 0;
	if (l <= t[p].l && t[p].r <= r) return t[p].sum;
	int res = 0;
	int mid = (t[p].l + t[p].r) >> 1;
	if (l <= mid) res += query(lp, l, r);
	if (r > mid) res += query(rp, l, r);

	return res;
}
void dfs1(int u, int fa) {
	a[u] = -query(1, 1, u - 1);
	b[u] = -query(1, 1, fa - 1);

	for (re i = h[u]; i; i = e[i].next) {
		int v = e[i].to;
		if (v == fa) continue;
		dfs1(v, u);
	}
	update(1, u, 1);
	a[u] += query(1, 1, u - 1);
	b[u] += query(1, 1, fa - 1);

	f[u] = a[u];
	for (re i = h[u]; i; i = e[i].next) {
		int v = e[i].to;
		if (v == fa) continue;
		f[u] += f[v];
	}
}
void dfs2(int u, int fa) {
	if (u != 1)
		g[u] = g[fa] - b[u] + (u - 1 - a[u]);
	for (re i = h[u]; i; i = e[i].next) {
		int v = e[i].to;
		if (v == fa) continue;
		dfs2(v, u);
	}
}
signed main() {
	cin >> n;
	for (re i = 1; i < n; i ++) {
		int x, y;
		cin >> x >> y;
		add(x, y), add(y, x);
	}
	build(1, 1, n);
	dfs1(1, 0);
	g[1] = f[1];
	dfs2(1, 0);
	for (re i = 1; i <= n; i ++) {
		cout << g[i] << ' ';
	}
	cout << '\n';
	return 0;
}

T5 P9726 [EC Final 2022] Magic


原题链接

这道题用到了网络流,不会网络流的看这里(网络流保姆级教程)我也刚又看一遍,才彻底通透。这道题还用到了dinic(点这里)
好了,前缀知识已经介绍差不多了,下面开始步入正题。

该说不说,道题是我感觉今天的题里除了国家集训队的题最难的一道了。

换⼀种⽅式考虑:
要选择若⼲个位置,使得这些位置最后贡献给答案,要求合法且选择的位置数
量最多。
对于区间 [ l 1 , r 1 ] 和 [ l 2 , r 2 ] , [l_1, r_1] 和 [l_2, r_2], [l1,r1][l2,r2] l 1 < l 2 < r 1 < r 2 l_1 < l_2 < r_1 < r_2 l1<l2<r1<r2,那么如果选择 l 2 l_2 l2,就意味着 [ l 1 , r 1 ] [l_1, r_1] [l1,r1]
要在 [ l 2 , r 2 ] [l_2, r_2] [l2,r2] 之前执⾏;同理如果选择 r 1 r_1 r1,就意味着 [ l 2 , r 2 ] [l_2, r_2] [l2,r2] 要在 [ l 1 , r 1 ] [l_1, r_1] [l1,r1] 之前执⾏。
所以 r 1 r_1 r1 l 2 l_2 l2不能同时选择,所以⼀定是⼀个建⽆向图后的独⽴集。

下面来证明一下这个结论是合法的:
只要选择独⽴集后不会成环,那么就能够说明这是合法的。
如果成环,假设环上选了⼀个区间 [ l , r ] [l,r] [l,r] 的右端点 r r r,导致它⽐某个区间 [ l ′ , r ′ ] [l',r'] [l,r]
晚执⾏ ( l < l ′ < r < r ′ ) 。 (l < l' < r < r')。 (l<l<r<r)
由于 l ′ l' l 不能被选择,那么下⼀个环上的点⼀定是由于选择了 r ′ r' r 导致的。
那么以此类推,在这条链上的区间 r r r ⼀定单调递增,所以不可能成环。

所以问题转化为了最⼤独⽴集问题。
⼜由于只有左端点和右端点之间的边,这是⼀个⼆分图最⼤独⽴集问题。
bitset 优化匈⽛利: O ( n 3 / w ) 。 O(n3/w)。 O(n3/w)
可持久化线段树优化建图 dinic: O ( n 2 l o g n ) O(n2log n) O(n2logn)

这就是这道题的实现思路了,代码里还有一些细节需要注意,自行观看。
c o d e code code

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define ll long long
using namespace std;
const int N = 5005;
int n, tot, l[N], r[N], match[N];
int px[N], py[N];
bitset<N>to[N], vs;
queue<int>Q;
int bfs(int u) {
	while (Q.size())Q.pop();
	vs.set(), Q.push(u);
	int v = -1;
	while (Q.size()) {
		int x = Q.front();
		Q.pop();
		bitset<N>tmp = vs & to[x];
		for (int y = tmp._Find_first(); y <= n; y = tmp._Find_next(y)) {
			int z = match[y];
			vs[y] = 0;
			if (z == 0) {
				match[y] = x, v = x;
				break;
			}
			Q.push(z), px[z] = x, py[z] = y;
		}
		if (~v)break;
	}
	if (v == -1)return 0;
	while (v != u) {
		match[py[v]] = px[v];
		v = px[v];
	}
	return 1;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> l[i] >> r[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (l[i] < l[j] && l[j] < r[i] && r[i] < r[j]) {
				to[i][j] = 1;
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		tot += bfs(i);
	}
	cout << 2 * n - tot;
}

总结

出的题很有难度,除了T1 ,T2 都不会,思路都不咋有,基本上都是一些算法的整合,综合运用,但还是有一些新的东西,比如换根dp之类的,那是真的不会。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值