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