2022.7.27好题选讲

T1 数学计算

  传送门:[TJOI2018]数学计算

t a g tag tag线段树维护操作序列

分析

  一开始看着这题好像没啥难度啊,直接模拟不就好了吗,为啥是蓝的呢?

  仔细一想,哇塞,这数据范围,要开高精了吧。再稍微算一下,哇塞,直接模拟高精空间就要炸了啊。所以直接算高精是不现实的了。

  再想想,一边算一边取模怎样呢,哇塞,有除法要算逆元欸。在看看数据,哇塞,模数不是质数欸。那么算逆元就变得相当麻烦了(而且还要特判整除)。

  那么我们显然不可能就直接模拟正解了,所以我们考虑一个比较暴力的模拟。我们维护一个操作序列,然后如果是操作 1 1 1 就一直乘下去。每次遇到一个操作 2 2 2,就删掉前面操作序列中的那个数,然后从头开始再做一遍前缀积。

  啊,这样一来,我们就考虑怎样能优化这个做法吧。想想刚才的暴力,肯定有很多重复计算,那么怎么避免呢。把前面算的数存下来?还是有很多无用功,再想想,维护一个数列,需要两个操作,第一个是把其中某一个数改成 1 1 1(就是题目中的操作 2 2 2),还有一个是在末尾加一个数(操作 1 1 1)。还需要从头到尾查前缀积,再抽象一点:支持单点修改,区间查询:线段树。

  那么这样,我们就能用线段树维护一个操作序列,每当操作 1 1 1 的时候就在序列末尾加上一个 m m m,每当操作 2 2 2 的时候就把 p o s pos pos 位置的数字改成 1 1 1,这样每次操作之后输出前缀积就做完了。

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
#define MAXN 100100
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)

inline int read(){
	int x = 0; char c = getchar();
	while(c < '0' or c > '9') c = getchar();
	while('0' <= c and c <= '9'){
		x = x * 10 + c - '0'; c = getchar();
	}
	return x;
}

int T = 0;
int q = 0; int M = 0;
int a[MAXN] = { 0 };
struct Tnode{
	int dat;
	int l, r;
}t[MAXN << 5];

inline void up(int p) { t[p].dat = (t[ls(p)].dat * t[rs(p)].dat % M) % M; }

void build(int p, int l, int r){
	t[p].l = l, t[p].r = r;
	if(l == r) { t[p].dat = 1; return; }
	int mid = (l + r) >> 1;
	build(ls(p), l, mid); build(rs(p), mid + 1, r);
	up(p);
}

void update(int p, int idx, int val){
	if(t[p].l == t[p].r) { t[p].dat = val; return; }
	int mid = (t[p].l + t[p].r) >> 1;
	if(idx <= mid) update(ls(p), idx, val);
	else update(rs(p), idx, val);
	up(p);
}

int query(int p, int l, int r){
	if(l <= t[p].l and t[p].r <= r) return t[p].dat;
	up(p);
	int mid = (t[p].l + t[p].r) >> 1, ans = 0;
	if(l <= mid) ans = (ans * query(ls(p), l, r) % M) % M;
	if(r > mid)  ans = (ans * query(rs(p), l, r) % M) % M;
	return ans;
}

signed main(){
	T = in;
	while(T--){
		q = in, M = in; build(1, 1, q);
		for(int i = 1; i <= q; i++){
			int op = in; a[i] = in;
			if(op == 1){
				update(1, i, a[i]);
				cout << query(1, 1, q) << '\n';
			}
			else{
				update(1, a[i], 1);
				cout << query(1, 1, q) << '\n';
			}
		}
	}
	return 0;
}

T2 跑步

[NOI Online #1 入门组] 跑步

t a g tag tag神秘的分块??根号分治?? or 数学???

分析

  题目简单说来就是让我们用一个非严格单调递减的数列来拆分 n n n,问有多少种分法。

  首先看到这题就一眼背包(完全背包), O ( n 2 ) O(n^2) O(n2),看看,已经 70 p t s 70pts 70pts 了,感觉剩下 30pts 性价比好低啊,要不不做了吧qwq。记前 i i i 数和为 j j j 的方案书为 f i , j f_{i, j} fi,j,那么:

f i , j = f i , j − 1 + f i − 1 , j f_{i, j} = f_{i, j - 1} + f_{i - 1, j } fi,j=fi,j1+fi1,j

  初始化 f 0 , 0 = 1 f_{0, 0} = 1 f0,0=1。再滚动数字组一下,甚至可以拿 80 p t s 80pts 80pts

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

int f[MAXN] = { 0 };
int n = 0; int p = 0;

int main(){
	cin >> n >> p;
	f[0] = 1;
	for(int i = 1; i <= n; i++)
		for(int j = i; j <= n; j++)
			f[j] = (f[j - i] + f[j]) % p;
	cout << f[n] << '\n';
	return 0;
} 

  但是我们还是要追求正解的,所以我么考虑优化。

  优化这个 d p dp dp 显然已经不能从转移入手了,已经是 O ( 1 ) O(1) O(1) 了还优化个寂寞。所以就只能从状态数入手。这样就要引入这道题的重点,也就是根号分治。

  我们把所有数分成两部分,一部分是数值 < n < \sqrt n <n 的,另一部分是 ≥ n \geq \sqrt n n 的。那么我们考虑把两部分分开计算再合并到一起。

  1. 数值 < n < \sqrt n <n 的部分,这里数字的种类数就是 n \sqrt n n ,所以如果按照上面暴力的做法 d p dp dp 的话 i i i 的枚举范围就变成了 1 ∼ n 1 \sim \sqrt n 1n ,这就是一个可以接受的范围,复杂度就是 O ( n n ) O(n\sqrt n) O(nn )
  2. 数值 ≥ n \geq \sqrt n n 的部分,这一部分的数字显然选的数的个数要 ≤ n \leq \sqrt n n ,因为这一部分的所有数值都 ≥ n \geq \sqrt n n ,如果个数也 > n > \sqrt n >n ,那么加起来就肯定大于 n n n 了。所以如果我们要像前一种情况一样降低状态数的话我们就要换一种方式 d p dp dp了。
    我们令 g i , j g_{i, j} gi,j 表示选了 i i i 数,和为 j j j 的方案数,这样就能利用个数不大于 n \sqrt n n 这个性质而进行 d p dp dp 了。
    现在就是要考虑如何进行这个 d p dp dp 了,我们考虑怎样构造出这样的数列。我们考虑两个操作,第一个是在数列中加入一个 n \sqrt n n (加入最小值),另一个是把数列中已有的数全部加 1 1 1。这两个操作交替进行(当然也可以某个操作连续执行很多次),就能够造出一个非严格单调递增的且最小值是 n \sqrt n n 的数列,于是我们就用 d p dp dp 来实现这两个操作:
    加入 n : g i , j + = g i − 1 , j − n 所有数加 1 : g i , j + = g i , j − i \begin{aligned} 加入 \sqrt n : & g_{i, j} +=g_{i - 1, j - \sqrt n} \\ 所有数加1 : & g_{i, j} += g_{i, j - i} \end{aligned} 加入n 所有数加1gi,j+=gi1,jn gi,j+=gi,ji
    这样一来,就完成我们所需要的转移了。

  最后就是一个比较简单的背包合并就好了。

代码

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100100
#define MAXM 505

int n = 0; int p = 0;
int f[MAXN] = { 0 };
int g[MAXM][MAXN] = { 0 };

int main(){
	cin >> n >> p; int ans = 0;
	int m = sqrt(n) + 1;
	f[0] = 1;
	for(int i = 1; i < m; i++)
		for(int j = i; j <= n; j++)
			f[j] = (f[j - i] + f[j]) % p;
	g[0][0] = 1;
	for(int i = 1; i <= m; i++)
		for(int j = i; j <= n; j++){
			g[i][j] = (g[i][j] + g[i][j - i]) % p;
			if(j >= m) g[i][j] = (g[i][j] + g[i - 1][j - m]) % p;
		}
	for(int i = 1; i <= m; i++)
		for(int j = 1; j <= n; j++)
			g[0][j] = (g[0][j] + g[i][j]) % p;
	for(int i = 0; i <= n; i++)
		ans = (ans + 1ll * g[0][i] * f[n - i] % p) % p;
	cout << ans << '\n';
	return 0;
}

T3 表达式求值

  传送门:P7324 [WC2021] 表达式求值

  我感觉我没怎么听懂…

T4 Poor Turkeys

  传送门:AT2389 [AGC016E] Poor Turkeys

t a g tag tag时光倒流???

分析

  正着做的话就是要满足一堆限制的情况下判断 ( i , j ) (i, j) (i,j) 合不合法,这样看起来就十分难做,所以我们考虑反着来。反着来的话就只需要满足 ( i , j ) (i, j) (i,j) 都存活的条件下判断前面一堆东西合不合法,看上去就会更加简单一些。

  所以我们考虑如果只让 i i i 存活有哪些条件。我们从最后一个指令往前追溯,如果遇到的指令和 i i i 无关,比如一个 ( x , y ) (x, y) (x,y),那么这两只鸡就随便杀,因为他们俩的死活不会影响到 i i i。但是如果遇到一个指令是 ( i , j ) (i, j) (i,j),这里我们就不能杀 i i i,只能杀 j j j。但是 j j j 可能在之前的某一步就已经寄了,所以我们不仅要在这一步咔嚓掉 j j j,还要保证在之前的指令中 j j j 不会被咔嚓掉。那么就相当于在前面的指令中要求 i , j i, j i,j 都要存活。

  于是我们就能想到维护一个 “保护集” S i S_i Si,表示要使得 i i i 最终存活下来,在前若干步中需要保护的鸡的集合,我们注意到,保护除了 i i i 之外的鸡都是为了在某一步把它干掉,所以集合中的元素能分成 i i i 和除了 i i i 的元素两部分。

  同理,我们从 j j j 往上走也能维护出一个 “保护集” S j S_j Sj,如果我们要 ( i , j ) (i, j) (i,j) 都存活,那么我们就要求 S i ⋂ S j = Φ S_i\bigcap S_j = \Phi SiSj=Φ

  这样一来,我们就能 O ( n m ) O(nm) O(nm) 处理出所有鸡的 “保护集”,然后 O ( n 2 ) O(n^2) O(n2),枚举所有点对,然后每个点对取交集判断一下就搞定了。

代码

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define MAXN 404
#define MAXM 100100

inline int read(){
	int x = 0; char c = getchar();
	while(c < '0' or c > '9') c = getchar();
	while('0' <= c and c <= '9'){
		x = x * 10 + c - '0'; c = getchar();
	} 
	return x;
}

int n = 0; int m = 0;
struct Trequ{
	int x, y;
}req[MAXM];
bitset<MAXN> s[MAXN];
bool flag[MAXN] = { 0 };

int main(){
	n = in; m = in;
	for(int i = 1; i <= m; i++) req[i].x = in, req[i].y = in;
	for(int i = 1; i <= n; i++) s[i][i] = 1;
	for(int i = 1; i <= n; i++)
		for(int j = m; j >= 1; j--){
			int x = req[j].x, y = req[j].y;
			if(s[i][x] && s[i][y]) { flag[i] = 1; break; }
			if(s[i][x] || s[i][y]) s[i][x] = s[i][y] = 1;
		}
	int ans = 0;
	for(int i = 1; i <= n; i++){
		if(flag[i]) continue;
		for(int j = i + 1; j <= n; j++){
			if(flag[j]) continue;
			if((s[i] & s[j]).count() == 0) ans++;
		}
	}
	cout << ans << '\n'; 
	return 0;
}

T5 Both Sides Merger

  传送门:AT3944 [ARC092C] Both Sides Merger

t a g tag tag考虑答案的来源

分析

  遇到这种操作的题下不要慌,手玩几个样例找找规律。这里就不举例子了,我们可以发现,在这些点中,对答案产生贡献的就是奇或偶位上的子序列。 也就是说要么在奇数位上,要么在偶数位上,不可能同时在奇数和偶数位置上都对答案有贡献。

  这样一来,我们就把问题转化成了一个最大子序列的问题,对于奇数位和偶数位分别跑一遍,然后再取 m a x max max 就做完了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
#define MAXN 1010
#define INFI 0x3f3f3f3f

inline int read(){
	int x = 0; int f = 1; char c = getchar();
	while(c < '0' or c > '9'){
		if(c == '-') f = -1; c = getchar();
	}
	while('0' <= c and c <= '9'){
		x = x * 10 + c - '0'; c = getchar();
	} 
	return f * x;
}

int a[MAXN] = { 0 };
int n = 0; int m = 0;
int ans1 = 0, ans2 = 0;
int us1[MAXN], us2[MAXN];

int tot = 0;
int c[MAXN] = { 0 };
int from[MAXN] = { 0 };
void solve(int cnt, int us[]){
//	for(int i = 1; i <= n; i++) cout << us[i] << ' '; puts("");
	int temp = 0;
	for(int i = 1; i <= n; i++)
		if(us[i]) from[i] = temp, temp = i;
//	for(int i = 1; i <= n; i++) cout << from[i] << ' '; puts("");
	int l = 1, r = cnt;
	for(int i = 1; i <= n; i++) 
		if(!us[i]) c[++tot] = 1, cnt--, l++;
		else break;
	for(int i = n; i >= 1; i--)
		if(!us[i]) c[++tot] = cnt, cnt--, r--;
		else break;
//	for(int i = 1; i <= tot; i++) cout << c[i] << ' '; puts("");
	int ll = l, rr = r;
	for(int i = l; i <= r; i++){
//		printf("i = %d\n", i);
		if(!from[i]) continue;
//		puts("  in");
		int x = i - ll + 1, y = from[i] - ll + 1;
//		printf("  x = %d y = %d\n", x, y);
		while(x != y){
			int mid = (x + y) >> 1;
			c[++tot] = mid; x -= 2, ll += 2;
//			printf("mid = %d\n", mid);
		}
	}
	cout << tot << '\n';
	for(int i = 1; i <= tot; i++) cout << c[i] << '\n';
}

signed main(){
	n = in; int ans = -INFI, f = 0, all = 1;
	for(int i = 1; i <= n; i++) { a[i] = in; if(a[i] > 0) all = 0; }
	if(all == 1){
		int ff = 0;
		ans1 = -INFI, ans2 = -INFI;
		for(int i = 1; i <= n; i += 2)
			if(a[i] > ans1) ans1 = a[i], ff = i;
		us1[ff] = 1;
		for(int i = 2; i <= n; i += 2)
			if(a[i] > ans2) ans2 = a[i], ff = i;
		us2[ff] = 1;
	}
	else{
		ans1 = 0, ans2 = 0;
		for(int i = 1; i <= n; i += 2)
			if(a[i] > 0) ans1 += a[i], us1[i] = 1;
		for(int i = 2; i <= n; i += 2)
			if(a[i] > 0) ans2 += a[i], us2[i] = 1;
	}
	if(ans1 > ans2) ans = ans1, f = 1;
	else ans = ans2, f = 2;
	cout << ans << '\n';
	if(f == 1) solve(n, us1);
	else if(f == 2) solve(n, us2);
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值