CSP-J 2020 简单题解

00:吐槽

今年 \(\texttt{PJ}\) 难度普遍偏低,\(\texttt{T3}\) 质量还不错。

总结来讲:做法显然、暴力踩正解。

01:优秀的拆分 / power

结论题。

\(n\) 为奇数时,无解:因为只有奇数的最低位为 \(1=2^0\)

否则从高位到低位枚举输出就可以了,时间复杂度 \(O(32)\);当然我用的是 \(\texttt{lowbit}\) 运算。

#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;

int stk[64], top = 0;

int main() {
	freopen("power.in", "r", stdin);
	freopen("power.out", "w", stdout);
	int x; scanf("%d", &x);
	if(x & 1) puts("-1");
	else {
		for( ; x; x -= lowbit(x))
			stk[++top] = lowbit(x);
		while(top--)
			printf("%d ", stk[top + 1]);
	}
	return 0;
}

02:直播获奖 / live

算法一(50pts)

依据题意直接 \(O(n^2)\) 暴力去找就可以了。

注意题目中所说的

在计算计划获奖人数时,如用浮点类型的变量(如 C/C++中的 float、double,Pascal 中的 real、double、extended 等)存储获奖比例 𝑤%,则计算 5 × 60% 时的结果可能为 3.000001,也可能为 2.999999,向下取整后的结果不确定。因此,建议仅使用整型变量,以计算出准确值。

都是废话,该怎么用还是怎么用。

算法二(100pts)

注意到每个人的分数值都在 \(600\) 以内,因此我们可以考虑 \(O(n)\) 的排序:桶排。

因为桶排是支持动态插入的,所以可以做这个题目,剩下的依据题意模拟即可,时间复杂度 \(O(600n)\)

据说有原题,代码就不放了。

算法三(100pts)

考虑题目所要求的的条件,即每次插入一个数,求其中的第 \(k\) 大,可以想到权值线段树。

注意查询的时候查询的是第 \(k\) 小,因此要注意转换成第 \(k\) 大,还有要记得离散化。

时间复杂度 \(O(n\log n)\)

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

const int N = 1e5 + 10;
const int T = N << 2;

#define ls(x) son[x][0]
#define rs(x) son[x][1]

int son[T][2], val[T];

int Newnode() {
	static int cnt = 0;
	return ++cnt;
}

void update(int p) {
	val[p] = val[ls(p)] + val[rs(p)];
}

void insert(int &p, int l, int r, int x) { 
	if(!p) p = Newnode();
	if(l == r) return void(++val[p]);
	int mid = (l + r) >> 1;
	if(x <= mid) insert(ls(p), l, mid, x);
	else insert(rs(p), mid + 1, r, x);
	update(p); 
}

int find(int p, int l, int r, int k) { 
	if(l == r) return l;
	int mid = (l + r) >> 1;
	if(k <= val[ls(p)]) 
		return find(ls(p), l, mid, k);
	return find(rs(p), mid + 1, r, k - val[ls(p)]);
}

int a[N], b[N];

int main() {
	freopen("live.in", "r", stdin);
	freopen("live.out", "w", stdout);
	int n, w, m, root = 0;
	scanf("%d %d", &n, &w);
	for(int i = 1; i <= n; i++)
		scanf("%d", a + i), b[i] = a[i];
	sort(b + 1, b + n + 1);
	m = unique(b + 1, b + n + 1) - (b + 1);
	for(int i = 1, x; i <= n; i++) {
		x = lower_bound(b + 1, b + m + 1, a[i]) - b;
		insert(root, 1, m, x);
		x = floor(1.0L * w * i / 100.0);
		printf("%d ", b[find(root, 1, m, i - max(1, x) + 1)]);
	}
	return 0;
}

03:表达式 / expr

算法一(30pts)

每次修改暴力修改,然后重复栈的过程,时间复杂度 \(O(q|S|)\)

算法二(100pts)

考虑每次修改一个点对答案的影响。

把原来给的后缀表达式建成表达式树,记 \(son_{x,0/1}\) 表示编号为 \(x\) 的节点的左/右儿子。如果当前节点是符号 \(!\) 的话那么只有左儿子。

\(f_i=0/1\) 表示这个节点的值取反后对答案有/无影响。

对于表达式中每一个数字,都用其原来的编号,符号节点新建编号,即表达式树的根的编号为 \(m\)

表达式树的所有叶子节点都是数值,非叶子节点都是符号,那么后缀表达式的最后一个符号就是表达式树的根,显然 \(f_m=1\)

接下来考虑标记的下传,记当前节点为 \(x\)

1、当前符号为 \(!\),那么其子节点的 \(f\)\(1\),否则为 \(0\)

2、当前符号为 \(\&\),那么若两个儿子节点的值均为 \(1\),则两个子节点的 \(f\) 均为 \(1\);若只有一个儿子节点的值为 \(1\),则为 \(0\) 的儿子节点的 \(f\)\(1\);其余情况子节点的 \(f\) 均为 \(0\)

3、当前符号为 \(|\),那么若两个儿子节点的值均为 \(0\),则两个子节点的 \(f\) 均为 \(1\);若只有一个儿子节点的值为 \(1\),则为 \(1\) 的儿子节点的 \(f\)\(1\);其余情况子节点的 \(f\) 均为 \(0\)

显然,\(a\&b\) 中,若两个都为 \(1\),则改变 \(a,b\) 任意一者的值均会改变结果;若只有一个为 \(1\),则只有那个为 \(0\) 的数变为 \(1\) 才会使结果由 \(0\) 变为 \(1\);否则(两个均为 \(0\))改变其中任何一个都对结果没有影响。符号为 \(|\) 同理可推出。

那么修改一个节点后的答案,即为节点 \(m\) 的值 异或 当前修改节点的 \(f\)

时间复杂度 \(O(|S|+q)\)

#include <bits/stdc++.h>

#define ls(x) son[x][0]
#define rs(x) son[x][1]

using namespace std;

const int N = 1e6 + 10;

char c[N];
int son[N][2], val[N], a[N], op[N];
int stk[N], top, n;
bool f[N];

void build(int p) {
	if(op[p] == 0) 
		return void(f[p] = 1);
	if(op[p] == 3) 
		return void(build(son[p][0]));
	if(op[p] == 1) {
		if(val[ls(p)] and val[rs(p)]) 
			build(ls(p)), build(rs(p));
		else if(val[ls(p)]) build(rs(p));
		else if(val[rs(p)]) build(ls(p));
		return ;
	} else {
		if(val[ls(p)] and !val[rs(p)])
			build(ls(p));
		else if(val[rs(p)] and !val[ls(p)])
			build(rs(p));
		else if(!val[ls(p)] and !val[rs(p)])
			build(ls(p)), build(rs(p));
		return ;
	}
}

int main() {	
	freopen("expr.in", "r", stdin);
	freopen("expr.out", "w", stdout);
	scanf("%[^\n]", c + 1);
	int size = strlen(c + 1), q, m;
	scanf("%d", &n), m = n;
	for(int i = 1; i <= n; i++)
		scanf("%d", a + i);
	for(int i = 1; i <= size; i++) {
		if(c[i] == 'x') {
			int j = i + 1, x = 0;
			while(isdigit(c[j])) 
				x = x * 10 + (c[j] ^ 48), j++;
			i = j;
			stk[++top] = x, val[x] = a[x];
		} else if(c[i] == '!') {
			int p = ++m;
			son[p][0] = stk[top--];
			val[p] = !val[son[p][0]];
			stk[++top] = p, op[p] = 3, i++;
			
		}
		else {
			int p = ++m;
			son[p][0] = stk[top--];
			son[p][1] = stk[top--];
			if(c[i] == '&') 
				val[p] = val[ls(p)] & val[rs(p)], op[p] = 1;
			else val[p] = val[ls(p)] | val[rs(p)], op[p] = 2;
			stk[++top] = p, i++;
		}	
	} 
	build(m);
	scanf("%d", &q);
	while(q--) {
		scanf("%d", &n);
		printf("%d\n", val[m] ^ f[n]);
	}
	
	
	return 0;
}

算法三(玄学)

注意到修改一个点只会修改一条链的值,如果数据比较水的话直接整就过了。

如果数据随机,均摊复杂度是 \(O(q\log |S|)\) 的。

04:方格取数 / number

算法一(20pts)

注意到 \(n,m\) 都很小,可以直接搜索解决。

算法二 (40pts)

\(n,m\) 也不是很大,可以搜索+剪枝解决。

当然值得提出的,这个数据是可以用网络流解决的。将每个位置拆成两个点,一个是不取的点,一个是要取的点,然后按照题目所给的能走到的就连边,跑最大费用最大流即可。

如果数据不怎么卡的话甚至可以过掉 \(70\) 分的数据。

算法三(70pts)

可以考虑最长路解决,但是可能被卡,所以实际得分不一定会有 \(70\),如果写得好会稳一些。

算法四(70pts)

考虑不会做的题就 \(\texttt{dp}\)

因为水平方向只有向左走,所以水平的行走(按列行走)是没有后效性的。

\(sum_{i,j}\) 表示走到第 \(i\) 列,前 \(j\) 行的 \(a_i\) 的前缀和,\(f_{i,j}\) 表示走到点 \((i,j)\) 所能达到的最大值,答案即为 \(f_{n,m}\)

考虑按列转移,当前为第 \(i\) 行第 \(j\) 列,每次枚举转移点 \(k\)(第 \(k\) 行):

1、\(k<i\)\(f_{i,j}=\max \{f_{i,j},f_{k,j-1}+ sum_{j,i}- sum_{j,k-1}\}\)

2、\(k=i\)\(f_{i,j}=\max \{f_{i,j},f_{i,j-1}+a_{i,j}\}\)

3、\(k>i\)\(f_{i,j}=\max \{f_{i,j},f_{k,j-1}+ sum_{j,k}- sum_{j,i-1}\}\)

时间复杂度 \(O(n^2m)\)

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

const int N = 1e3 + 10;

int a[N][N], f[N][N], sum[N][N];

int main() {
	int n, m;
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			scanf("%d", a[i] + j);
	f[1][1] = a[1][1];
	for(int i = 2; i <= n; i++)
		f[i][1] = f[i - 1][1] + a[i][1];
	for(int j = 2; j <= m; j++)
		for(int i = 1; i <= n; i++)
			sum[i][j] = sum[i - 1][j] + a[i][j];
	for(int j = 2; j <= m; j++) {
		
		for(int i = 1; i <= n; i++) {
			f[i][j] = f[i][j - 1] + a[i][j];
			for(int k = 1; k < i; k++) 
				f[i][j] = max(f[i][j], f[k][j - 1] + sum[i][j] - sum[k - 1][j]);
			for(int k = i + 1; k <= n; k++)
				f[i][j] = max(f[i][j], f[k][j - 1] + sum[k][j] - sum[i - 1][j]);	
		}
		
	}
	printf("%d\n", f[n][m]);
	return 0;
}

算法五(100pts)

显然上面的算法需要一个简单的优化,注意到我们一直在重复累加一些值,事实上是在对上一列的 \(f\) 做前/后缀和。

\(f_{i,j,0/1/2}\) 表示从左、上、下走来,那么有转移:

1、\(f_{i,j,0}=\max(f_{i,j+1,0},f_{i-1,j,1},f_{i+1,j,2})+a_{i,j}\)

2、\(f_{i,j,1}=\max(f_{i,j+1,0},f_{i-1,j,1})+a_{i,j}\)

3、\(f_{i,j,2}=\max(f_{i,j+1,0},f_{i+1,j,2})+a_{i,j}\)

边界:

\(f_{i,m+1,k}=f_{n+1,j,k}=f_{0,j,k}=f_{i,0,k}=-\inf\)

答案即为 \(\max(f_{1,1,0},f_{1,1,1},f_{1,1,2})\)

其实这也可以理解为上面方程的前缀和优化,时间复杂度 \(O(nm)\)

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

const long long inf = 1LL << 60;
const int N = 1e3 + 10;

long long a[N][N], f[N][N], g[N][N][2];

int main() {
	freopen("number.in", "r", stdin);
	freopen("number.out", "w", stdout);
	int n, m;
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			scanf("%lld", a[i] + j);
	for(int i = 0; i <= n + 1; i++)
		for(int j = 0; j <= m + 1; j++)
			f[i][j] = g[i][j][0] = g[i][j][1] = -inf;
	f[1][1] = g[1][1][0] = g[1][1][1] = a[1][1];
	
	for(int j = 1; j <= m; j++) {
		for(int i = 1; i <= n; i++) 
			f[i][j] = max(max(f[i][j], f[i][j - 1] + a[i][j]), max(g[i][j - 1][0], g[i][j - 1][1]) + a[i][j]);	
		for(int i = 1; i <= n; i++)
			g[i][j][0] = max(max(g[i - 1][j][0], f[i - 1][j]) + a[i][j], g[i][j][0]);
		for(int i = n; i; i--)
			g[i][j][1] = max(max(g[i + 1][j][1], f[i + 1][j]) + a[i][j], g[i][j][1]);
	}
	printf("%lld\n", max(f[n][m], max(g[n][m][0], g[n][m][1])));
	return 0;
}
posted @ 2020-11-10 13:13  Ning-H  阅读(733)  评论(1编辑  收藏  举报