正睿 OI 好题总结

区间逆序对

区间逆序对
\qquad 对于 S u b t a s k 1 Subtask1 Subtask1,自然直接 O ( n 2 ) O(n^2) O(n2) 枚举即可;对于 S u b t a s k 2 Subtask2 Subtask2,用莫队也是可以轻易拿到这档分的。那么正解该怎么写呢?

\qquad 首先,我们品读题目,“区间逆序对”,区间这个字眼是不是十分敏感呢?查询区间信息,最先想到的一定是前缀和了。再次仔细观察题面,发现 a i ≤ 50 a_i\leq50 ai50。这是一个突破口。有了前缀和的想法,加上 a i ≤ 50 a_i\leq50 ai50,就不难想到:用 p i p_i pi 表示前 i i i 个数构成的逆序对数, s u m i , j sum_i,_j sumi,j 表示前 i i i 个数中,数 j j j 出现次数的前缀和,将 p , s u m p, sum p,sum 数组预处理出来。然后,就该考虑如何处理对区间 [ l , r ] [l,r] [l,r] 的询问了。这时,我们考虑将前缀 [ 1 , r ] [1,r] [1,r] 中的逆序对分成三部分:前缀 [ 1 , l ) [1,l) [1,l) 中的逆序对,区间 [ l , r ] [l,r] [l,r] 中的逆序对,前数在 [ 1 , l ) [1,l) [1,l) 中,后数在 [ l , r ] [l,r] [l,r] 中拼成的逆序对。不难发现, [ l , r ] [l,r] [l,r] 中的逆序对等于 p r − p l − 1 p_{r} - p_{l-1} prpl1 − - 左右拼接的逆序对数。左右拼接的逆序对数怎么求呢?显然是从 1 ∼ 50 1\thicksim50 150 枚举每个数,看 [ 1 , l ) [1,l) [1,l) 中比它大的数出现了几次。不难发现,可以倒序枚举,再加一个小的前缀和优化。就这样,这道题就完美解决了。

\qquad 注意数据范围,考虑每道题为什么给这么一个数据范围。

\qquad C o d e : Code: Code:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int maxn = 1e6 + 10;
const int maxa = 60;
int n, m;
int a[maxn], num[maxn][maxa];
LL p[maxn];

inline int read() {
	int x = 0; char ch = getchar();
	while(!isdigit(ch)) ch = getchar();
	while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
	return x;
}
	
void sol() {
	for(int i = 1; i <= n; i ++) {
		for(int j = 1; j < maxa; j ++) num[i][j] = num[i - 1][j];
		int cnt = 0;
		for(int j = a[i] + 1; j < maxa; j ++) cnt += num[i][j];
		p[i] = p[i - 1] + 1LL * cnt;
		num[i][a[i]] ++;
	}
	for(int i = 1, l, r; i <= m; i ++) {
		l = read(), r = read();
		LL cnt = 0, res = 0;//res:小的前缀和优化
		for(int j = maxa - 1; j >= 1; j --) cnt += res * 1LL * (num[r][j] - num[l - 1][j]), res += 1LL * num[l - 1][j];
		printf("%lld\n", p[r] - p[l - 1] - cnt);
	}
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i ++) a[i] = read() + 1;
	sol();
	return 0;
}

排列

排列
\qquad 看到这道题,我们就应想到一句老俗语:贪心只能过样例。那么,我们一点点想: 20 p t s 20pts 20pts 怎么写呢? d p dp dp。状态怎么设呢?按照惯(tao)例(lu),应该设 d p i , j dp_i,_j dpi,j 表示将数 i i i 移到位置 j j j,最小代价是多少。转移自然只能由 d p i − 1 , 1 ∼ m dp_{i-1},_{1\thicksim m} dpi1,1m 转移过来,最后答案就是 m i n ( d p n , 1 ∼ m ) min(dp_n,_{1\thicksim m}) min(dpn,1m)

\qquad 有了上面的思路,我们想如何优化。上面的瓶颈在于枚举 i − 1 i-1 i1 i i i 的位置,此过程时间复杂度为 O ( m 2 ) O(m^2) O(m2)。现在,我们想:若 i − 1 i-1 i1 的位置已确定,而且此时 i i i 不在 [ 1 , m ] [1,m] [1,m] 内,如何移动才能使结果最优?不难想到:让 i i i 移到 1 1 1 m m m 最优。类似的,若 i − 2 i-2 i2 的位置已确定,而且此时 i − 1 i-1 i1 不在 [ 1 , m ] [1,m] [1,m] 内,怎么办?如此递归想下去,我们推出一个性质: i i i 不在 1 , m 1,m 1,m 内,那么最优操作一定是将 i i i 移到 1 1 1 m m m 有了这个性质,我们就考虑优化一下我们的状态: d p i , 0 / 1 dp_{i},_{0/1} dpi,0/1 表示将 i i i 移到 1 ( 0 ) 1(0) 1(0) m ( 1 ) m(1) m(1),最小代价是多少。现在考虑转移。

\qquad 若你率尔而对曰:从 d p i , 0 / 1 dp_{i},_{0/1} dpi,0/1 转移到 d p i + 1 , 0 / 1 dp_{i+1},_{0/1} dpi+1,0/1,那么恭喜你收获了和我赛时一样的错误思路。为什么错呢?我们想,在什么情况下 d p i , 0 / 1 dp_{i},_{0/1} dpi,0/1 会转移给 d p i + 1 , 0 / 1 dp_{i+1},_{0/1} dpi+1,0/1?若当 i i i 位置确定的时候, i + 1 i+1 i+1 也在 [ 1 , m ] [1,m] [1,m] 中,那么 i i i 的状态还用转移给 i + 1 i+1 i+1 吗?显然不用。那么需要转移给谁呢?通过上面的分析我们不难发现,应转移给一个 k k k,这个 k k k 要满足: k k k i i i 位置确定后,处于 [ 1 , m ] [1,m] [1,m] 外的最小的大于 i i i 的数。此时意味着 [ i + 1 , k − 1 ] [i+1,k-1] [i+1,k1] 中的数在 i i i 的位置确定时,也都处于 [ 1 , m ] [1,m] [1,m] 内。由状态得: i i i 的位置确定只有两种情况: i i i 1 1 1 i i i m m m,那么这两种情况对应的 k k k 也就如下图:
i在1的位置

i在m的位置
\qquad 这个 k k k 该怎么找呢?线段树显然可以搞。因为我们是从 1 ∼ n 1\thicksim n 1n 枚举的,每次找的还都是最小的大于 i i i 的,那么我们可以每次处理完一个 i i i,把 i i i 这个位置上的数改为极大值,然后每次查询指定区间内的最小值就是要找的 k k k。这道题就这么解决了。

\qquad C o d e : Code: Code:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int maxn = 5e5 + 10;
int n, m;
int a[maxn << 1], loc[maxn];
struct Segment {
	int l, r, dat;
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define dat(x) tree[x].dat
}tree[maxn << 3];
LL dp[maxn][2];

inline int read() {
	int x = 0; char ch = getchar();
	while(!isdigit(ch)) ch = getchar();
	while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
	return x;
}

inline void update(int p) {
	dat(p) = min(dat(p << 1), dat(p << 1 | 1));
}

void build(int p, int l, int r) {
	l(p) = l, r(p) = r;
	if(l == r) {dat(p) = a[l]; return ;}
	int mid = l + r >> 1;
	build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
	update(p);
}

int query(int p, int l, int r) {
	if(l <= l(p) && r(p) <= r) return dat(p);
	int minn = maxn, mid = l(p) + r(p) >> 1;
	if(l <= mid) minn = min(minn, query(p << 1, l, r));
	if(r > mid) minn = min(minn, query(p << 1 | 1, l, r));
	return minn;
}

void modify(int p, int x, int val) {
	if(l(p) == r(p)) {dat(p) = val; return ;}
	int mid = l(p) + r(p) >> 1;
	if(x <= mid) modify(p << 1, x, val);
	else modify(p << 1 | 1, x, val);
	update(p);
}

int Get(int loc, int x) {//Get(i,j):从i到j最小距离
	if(loc <= 0) loc += n;
	if(loc > n) loc -= n;
	int lft = (loc >= x ? loc - x : loc + n - x), rgh = (loc <= x ? x - loc : n - loc + x);
	//后面要减,所以为了方便在这里加一个正负号
	if(lft < rgh) return lft;//正左
	else return -rgh;//负右
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i ++) a[i] = read(), loc[a[i]] = i, a[i + n] = a[i];//直接建环,方便查询
	build(1, 1, n << 1);
	memset(dp, 0x7f, sizeof dp);
	dp[0][0] = 0, loc[0] = 1;//初始化,loc[0]=1是为了方便操作
	LL ans = 0x7f7f7f7f7f7f7f7f;
	for(int i = 0; i < n; i ++) {
		//consider 0
		int minn = query(1, loc[i] + m, loc[i] + n - 1);//找k,loc[0]=1是为了方便这里
		if(minn == maxn) ans = min(ans, dp[i][0]);
		else {
			dp[minn][0] = min(dp[minn][0], dp[i][0] + abs(Get(loc[minn] - Get(loc[i], 1), 1)));
			dp[minn][1] = min(dp[minn][1], dp[i][0] + abs(Get(loc[minn] - Get(loc[i], 1), m)));
		//consider 1
		minn = query(1, loc[i] + 1, loc[i] + n - m);
		if(minn == maxn) ans = min(ans, dp[i][1]);
		else {
			dp[minn][0] = min(dp[minn][0], dp[i][1] + abs(Get(loc[minn] - Get(loc[i], m), 1)));
			dp[minn][1] = min(dp[minn][1], dp[i][1] + abs(Get(loc[minn] - Get(loc[i], m), m)));
		}
		if(i) modify(1, loc[i], maxn), modify(1, loc[i] + n, maxn);
	}
	printf("%lld\n", min(ans, min(dp[n][0], dp[n][1])));
	return 0;
}

山茶花

山茶花
\qquad 首先,看到与多数异或和最大值有关就不难想到线性基,但这又跟普通线性基不同。因为中间有一个加一操作。我们想:如果中间没有加一操作,那么答案永远都是固定的,因为异或运算具有交换律和结合律。现在有了加一操作,而且只能用一次。我们想:在什么情况下,使用这个加一操作最优呢?不难发现,当它有一段后缀连续 1 1 1 ,并且这一串连续 1 1 1 最长的时候,加一操作是最优的(进位的效果最好)。有了这个结论,我们就发现:我们要优先考虑填充出连续的一段后缀 1 1 1,所以,顺理成章地,就能想到线性基倒着操作,从最低位开始,维护一个低位线性基。然后查找的时候从高位开始查找(因为后缀 1 1 1 越长越好)即可。

\qquad C o d e : Code: Code:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int maxn = 1e6 + 10;
int n;
LL a[maxn], b[65];

void Insert(LL x) {
	for(int i = 0; i <= 60; i ++) {//低位线性基,优先满足低位需求
		if((x >> i) & 1) {
			if(b[i]) x ^= b[i];
			else return b[i] = x, void();
		}
	}
}

LL query(LL x) {//判断能否构造出长度为x的后缀1
	LL res = 0;
	for(int i = 0; i <= x; i ++) {
		if(((res >> i) & 1) == (i != x)) continue;
		if(b[i]) res ^= b[i];
		else return -1;
	}
	return res;
}

int main() {
	scanf("%d", &n);
	LL ans = 0;
	for(int i = 1; i <= n; i ++) scanf("%lld", &a[i]), ans ^= a[i], Insert(a[i]);
	for(int i = 60; i >= 0; i --) {//越长越好
		if((ans >> i) & 1) continue;
		LL res = query(i);
		if(res != -1) {
			ans ^= ((1LL << (1LL * (i + 1))) - 1LL);//等价于:ans=((ans^res)^(res+1))
			printf("%lld\n", ans);
			return 0;
		}
	}
	printf("%lld\n", ans + 1LL);
	return 0;
}

Medians

Medians
\qquad 怎么说呢,本题其实有一点套路。但是这个套路较为常用且经典,所以就总结一下。也是场A了

\qquad 看到动态中位数,是不是一下子就想到了对顶堆呢?对顶堆显然是可以轻松拿到 60 p t s 60pts 60pts 的·。但是对于 n ≤ 1 e 7 n\leq1e7 n1e7 的数据,对顶堆也无能为力了。不过因为本题时限 4s,而且正睿少爷机跑得飞快,让许多选手水了过去那么正解该怎么做呢?

\qquad 首先,本题的要求相当于我们依次往序列里插入 a i a_i ai,并动态维护中位数。显然,插入数字是不好 O ( 1 ) O(1) O(1) 维护中位数的。那么我们考虑倒着做,每次从序列中删除一个数。而且我们发现,在序列有序的情况下,若以删数的方式来写,不仅好进行删数操作,且每次中位数只会向左或向右调整一个数。此时,又一个重要性质出现了:序列是一个排列!这意味着所有数值域都是 [ 1 , n ] [1,n] [1,n],而且互不相同。那么,一个可以在 O ( 1 ) O(1) O(1) 时间复杂度内维护前驱后继的数据结构——链表就映入了我们的脑海。我们开一个值域链表,并记录每个数的前驱后继,这样不仅可以保证序列有序,而且可以很方便的左右移动中位数。至此,本题就顺利在 O ( n ) O(n) O(n) 时间复杂度内解决了。

\qquad 本题套路在哪呢?首先,正难则反:把正着插入一个数变为倒着删除一个数;其次:排列。本题让我们意识到了排列的两个重要性质:1、值域为[1,n]2、数互不相同。这两个性质指引我们可以开一些基于值域操作的数据结构来辅助优化。

\qquad C o d e : Code: Code:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int maxn = 1e7 + 10;
const int mod = 998244353;
const int mod7 = 1e9 + 7;
const int mod9 = 1e9 + 9;
int n;
int p[maxn], h[maxn];
struct node {
	int pre, nxt;
}f[maxn];//值域链表

int main() {
	int a;
	scanf("%d%d", &n, &a);
	for(int i = 1; i <= n; i ++) p[i] = i;
	for(int i = 1; i <= n; i ++) a = (1LL * a * mod + 1LL * mod7) % (1LL * mod9), swap(p[i], p[a % i + 1]);
	h[0] = 1;
	for(int i = 1; i <= n; i ++) f[i].pre = i - 1, f[i].nxt = i + 1, h[i] = (1LL * h[i - 1] * 19LL) % mod;//一定要预处理出19^i,要是用快速幂就废了
	int ans = (n + 1) / 2, cnt = 0;
	int s = ans - 1, b = n - ans;//维护比当前中位数小、大的数有多少个
	for(int i = n; i >= 1; i --) {
		while(s + 1 < (i + 1) / 2) ans = f[ans].nxt, s ++, b --;
		while(s + 1 > (i + 1) / 2) ans = f[ans].pre, s --, b ++;//动态调整,最多一次
		cnt = (cnt + (1LL * (ans % mod) * h[i]) % mod) % (1LL * mod);
		int x = p[i];
		f[f[x].pre].nxt = f[x].nxt, f[f[x].nxt].pre = f[x].pre;
		if(x < ans) s --;
		else if(x == ans) {
			if(f[ans].pre == 0) ans = f[ans].nxt, b --;
			else ans = f[ans].pre, s --;
		}
		else b --;
	}
	printf("%d\n", cnt);
	return 0;
}

construct

construct
\qquad 怎么说呢,人类智慧构造题……

\qquad 先观察式子: x p 2 = a b + b c + a c xp^2=ab+bc+ac xp2=ab+bc+ac非常难看。我们考虑尽量让两边都变成因式相乘的形式,然后分配。

x p 2 = a b + b c + a c xp^2=ab+bc+ac xp2=ab+bc+ac
x p 2 = ( a + b ) ( a + c ) − a 2 xp^2=(a+b)(a+c)-a^2 xp2=(a+b)(a+c)a2
x p 2 + a 2 = ( a + b ) ( a + c ) xp^2+a^2=(a+b)(a+c) xp2+a2=(a+b)(a+c)

\qquad 到这一步,我们想:是不是可以直接给 a a a 赋值为 p p p 呢?

x p 2 + p 2 = ( b + p ) ( c + p ) xp^2+p^2=(b+p)(c+p) xp2+p2=(b+p)(c+p)
( x + 1 ) p 2 = ( b + p ) ( c + p ) (x+1)p^2=(b+p)(c+p) (x+1)p2=(b+p)(c+p)
b = x + 1 − p , c = p 2 − p b=x+1-p, c=p^2-p b=x+1p,c=p2p

\qquad 到了这一步,方案已经出来了。但是题上要求 a , b , c a,b,c a,b,c 均为正整数,但观察上面的方案,不难发现 b b b 可能为负数。所以还需进一步分类讨论。1、 x ≥ p x\geq p xp。此时 a , b , c a,b,c a,b,c 显然都是正整数,方案合法,直接输出(这也对应了题目上 S u n t a s k 3 Suntask 3 Suntask3,这告诉我们要尽量往数据范围上想,想题目为什么设置这档分);2、 x > p x>p x>p。对于这种情况,我们的处理方法是尽量往简单的情况上变,即想办法缩小 p p p 的范围。怎么缩小呢?取模! 为了贴合情况 1 1 1 p p p 的范围,我们令 m = p m=p%(x+1) m=p,这样 m ∈ [ 0 , x ] m\in[0,x] m[0,x]。我们仍旧直接给 a a a 赋值为 m m m。取模之后,我们就需将原来的等式变为同余式。

x p 2 + m 2 ≡ ( b + m ) ( c + m ) xp^2+m^2\equiv (b+m)(c+m) xp2+m2(b+m)(c+m) \qquad m o d ( x + 1 ) mod(x+1) mod(x+1)

\qquad 此时,我们对 b b b 仍按上面的方式赋值为 x − 1 + m x-1+m x1+m,然后反解出 c = x p 2 + m 2 x + 1 − m c=\frac{xp^2+m^2}{x+1}-m c=x+1xp2+m2m。但是但是,此时我们发现,虽然 b , c b,c b,c 都是正整数了,但 a a a 可能是负数——当 p = x + 1 p=x+1 p=x+1 时。然后,通过不懈的打表发现:当 a = 6 a=6 a=6 时,可以构造出一组解: b = p − 3 , c = p 2 − 4 p + 6 b=p-3,c=p^2-4p+6 b=p3,c=p24p+6。此时, c c c 恒大于 0 0 0,当 p ≤ 3 p\leq 3 p3 时, b ≤ 0 b\leq 0 b0,此时无解;否则 b > 0 b>0 b>0

\qquad 最终,这道人类智慧构造题也是顺利切掉了……

\qquad C o d e : Code: Code:

#include <bits/stdc++.h>
using namespace std;
#define int long long
int T, x, p;
signed main() {
	scanf("%lld", &T);
	while(T --) {
		scanf("%lld%lld", &x, &p);
		if(x >= p) printf("%lld %lld %lld\n", p, x + 1 - p, p * p - p);
		else {
			if(p - 1 == x) {
				if(p <= 3) puts("-1");
				else printf("6 %lld %lld\n", p - 3, p * p - 4 * p + 6);
			}
			else {
				int m = p % (x + 1);
				printf("%lld %lld %lld\n", m, x + 1 - m, (x * p * p + m * m) / (x + 1) - m);
			}
		}
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值