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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM