CSP-J 2020 编程算法比赛
作者:
| 更新日期:解析逻辑表达式
本文首发于公众号:天空的代码世界,微信号:tiankonguse
零、背景
最近我计划研究 CSP-J 与 CSP-S 的比赛题目,之前已经完成了 8 场比赛的题解,今天将分享 2020 年 CSP-J 第二轮编程算法比赛的详细题解。
A: 二进制
B: 暴力统计、二分线段树、分块、计数平衡树(splay/树堆等)
C: 语法树,贪心
D: 动态规划
代码地址: https://github.com/tiankonguse/leetcode-solutions/tree/master/other/CSP-J/
比赛题目分类与题解 |
---|
CSP-J 2024 题解 A:扑克牌 入门 B: 地图探险 普及− C: 小木棍 普及/提高− D: 接龙 提高+/省选− |
CSP-S 2024 题解 A:决斗 普及− B: 超速检测 普及+/提高 C: 染色 提高+/省选− D: 擂台游戏 NOI/NOI+/CTSC |
CSP-J 2023 题解 A:小苹果 普及− B: 公路 普及− C: 一元二次方程 普及/提高− D: 旅游巴士 普及+/提高 |
CSP-S 2023 题解 A:密码锁 普及− B: 消消乐 提高+/省选− C: 结构体 提高+/省选− D: 种树 提高+/省选− |
CSP-J 2022 题解 A:乘方 入门 B: 解密 普及− C: 逻辑表达式 普及+/提高 D: 上升点列 普及/提高− |
CSP-S 2022 题解 A:假期计划 提高+/省选− B: 策略游戏 普及+/提高 C: 星战 省选/NOI− D: 数据传输 省选/NOI− |
CSP-J 2021 题解 A:分糖果 普及− B: 插入排序 普及/提高− C: 网络连接 普及/提高− D: 小熊的果篮 普及+/提高 |
CSP-S 2021 题解 A:廊桥分配 普及+/提高 B: 括号序列 提高+/省选− C: 回文 普及+/提高 D: 交通规划 省选/NOI− |
CSP-J 2020 题解 A:优秀的拆分 入门 B: 直播获奖 普及− C: 表达式 普及+/提高 D: 方格取数 普及+/提高 |
一、优秀的拆分
题意:给一个正整数,求将其拆分为若干个不同的2的幂之和。如果存在答案,从大到小输出。
思路:二进制
一个数拆分为2的不同幂之和,显然就是其二进制表示。
要求幂是正整数,因此奇数无法拆分。
if (n % 2 == 1) {
printf("-1\n");
return;
}
vector<int> nums;
int bit = 0;
while (n > 0) {
if (n & 1) {
nums.push_back(1 << bit);
}
bit++;
n >>= 1;
}
std::reverse(nums.begin(), nums.end());
二、直播获奖
题意:比赛的前 w%
的选手获奖,现在依次告诉你一个选手的成绩,问目前获奖分数线是多少。
思路:统计
成绩的数据范围只有 600,最简单的方法是统计每个分数的人数,然后从大到小枚举分数,判断是否达到分数线。
复杂度:O(C*N)
vector<int> score(MaxScore + 1, 0);
for (int i = 1; i <= n; i++) {
scanf("%d", &x);
score[x]++;
int rank = max(1, i * w / 100);
int sum = 0;
int ans = 0;
for (int j = MaxScore; j >=0; j--) {
sum += score[j];
if (sum >= rank) {
ans = j;
break;
}
}
printf("%d%c", ans, i == n ? '\n' : ' ');
}
优化:可以通过二分线段树来求分数线。
复杂度:O(N * log(C) log(C))
优化:分块,即对分数划分若干个块,每个块内分别计数,并计算块的和。
复杂度:O(N * sqrt(C))
优化:计数类平衡树,即维护一个支持重复节点的搜索树。
复杂度:O(N * log(C))
三、表达式
题意:给一个后缀逻辑运算表示,表达式的叶子节点都是已知值的变量,现在随机对一个变量的值取反,问表达式的值是多少。
思路:语法树与贪心
表达式本质上是一个二叉树,故需要定义树的节点。
enum { E_VAL = 0, E_AND, E_OR, E_NOT };
struct Node {
int type; //
// int idx;
int val;
int left, right;
};
然后是节点分配器,通过下标来代替指针。
Node nodes[max6];
int nodeIdx = 0;
int NewNode(int type, int val = 0, int left = 0, int right = 0) {
int idx = ++nodeIdx;
nodes[idx].type = type;
// nodes[idx].idx = idx;
nodes[idx].val = val;
nodes[idx].left = left;
nodes[idx].right = right;
return idx;
}
最后是通过栈,来构造表达式语法树。
stack<int> sta;
bool AddNode() {
scanf("%s", buf);
const char c = buf[0];
if (c == 'x') {
int idx = NewNode(E_VAL, 0);
int name = ToInt(buf + 1);
nameToIdx[name] = idx;
idxToName[idx] = name;
sta.push(idx);
} else if (c == '|') {
int rightIdx = sta.top();
sta.pop();
int leftIdx = sta.top();
sta.pop();
int idx = NewNode(E_OR, 0, leftIdx, rightIdx);
sta.push(idx);
} else if (c == '&') {
int rightIdx = sta.top();
sta.pop();
int leftIdx = sta.top();
sta.pop();
int idx = NewNode(E_AND, 0, leftIdx, rightIdx);
sta.push(idx);
} else if (c == '!') {
int leftIdx = sta.top();
sta.pop();
int idx = NewNode(E_NOT, 0, leftIdx, 0);
sta.push(idx);
} else {
return false;
}
return true;
}
while (AddNode()) {
continue;
}
root = sta.top();
输入所有叶子节点变量的值后,就可以递归计算出表达式的值。
int DfsInit(int pre) {
if (nodes[pre].type == E_AND) {
int left = DfsInit(nodes[pre].left);
int right = DfsInit(nodes[pre].right);
return nodes[pre].val = left && right;
} else if (nodes[pre].type == E_OR) {
int left = DfsInit(nodes[pre].left);
int right = DfsInit(nodes[pre].right);
return nodes[pre].val = left || right;
} else if (nodes[pre].type == E_NOT) {
int left = DfsInit(nodes[pre].left);
return nodes[pre].val = 1 - left;
} else {
return nodes[pre].val;
}
}
如果暴力做这道题,复杂度是 O(q*n)
。
分析叶子节点对结果的影响情况,会发现,要么不影响答案不变,要么影响答案取反。
什么时候不影响答案呢?
与或运算被短路的时候。
故可以递归预处理表达式树,将所有被短路的子树全部标记为不影响答案,剩余的就是影响答案的节点。
void DfsFlag(int pre, bool flag) {
if (pre == 0) return;
if (flag) { // 子树全部不影响答案
if (nodes[pre].type == E_VAL) {
ignoreIdxs[pre] = 1;
}
DfsFlag(nodes[pre].left, flag);
DfsFlag(nodes[pre].right, flag);
return;
}
if (nodes[pre].type == E_AND) {
DfsFlag(nodes[pre].left, !RightVal(pre));
DfsFlag(nodes[pre].right, !LeftVal(pre));
} else if (nodes[pre].type == E_OR) {
DfsFlag(nodes[pre].left, RightVal(pre));
DfsFlag(nodes[pre].right, LeftVal(pre));
} else if (nodes[pre].type == E_NOT) {
DfsFlag(nodes[pre].left, false);
} else {
// do nothing
}
}
预处理后,就可以O(1)
处理询问了。
while (q--) {
int name;
scanf("%d", &name);
int idx = nameToIdx[name];
if (ignoreIdxs[idx]) {
printf("%d\n", nodes[root].val); // 这个位置变化不影响答案
} else {
printf("%d\n", 1 - nodes[root].val); // 这个位置影响答案,即翻转
}
}
四、方格取数
题意:给一个方格图,只能上下与向右走,同一个位置只能经过一次,问从左上角到达右下角的最大路径和。
思路:动态规划
只能从左向右,显然需要逐列处理,计算每一列的局部最优值。
状态定义:f(n,m)
从左上角到达位置(n,m)
,下一步只能向右走时的最大路径和。
状态转移方程:当前列每个位置可以从上一列的任何一个位置到达。
其中 Dis(m,i,n)
含义为第 m 列从 i 到达 n 的线段和。
f(n,m) = max(f(i, m-1) + Dis(m, i, n));
如果不做任何优化,复杂度是 O(n*m*n)
,n 比较大时会超时。
将状态转移方程展开,如下
f(a,b) = max(
f(0, b-1) + Dis(b, 0, a),
f(1, b-1) + Dis(b, 1, a),
f(2, b-1) + Dis(b, 2, a),
...
f(a-1, b-1) + Dis(b, a-1, a),
f(a, b-1) + Dis(b, a, a),
f(a+1, b-1) + Dis(b, a, a+1),
...
f(n-1, b-1) + Dis(b, a, n-1),
f(n, b-1) + Dis(b, a, n),
);
可以发现,状态转移方程分为两部分,第 a 行之上的是求上面路径的和,终点是 a,第 a 行之下的是求下面的路径之和,起点是 a。
上半部和下半部分开计算,令 f(a,b) = max(up(a,b), down(a,b))
然后可以发现,上半部可以复用相邻位置的信息。
up(a,b) = max(up(a-1,b), f(a, b-1)) + V;
down(a,b) = max(down(a+1, b), f(a.b-1)) + V;
故可以定义两个子状态,然后叠加求出父状态。
复杂度:O(n*m)
.
dp[0][1] = 0;
for (int i = 1; i <= n; i++) {
dp[i][1] = a[i][1] + dp[i - 1][1];
}
for (int j = 2; j <= m; j++) {
// 从上到下
ll maxValue = -INFL;
for (int i = 1; i <= n; i++) {
maxValue = max(maxValue, dp[i][j - 1]) + a[i][j];
dp[i][j] = max(dp[i][j], maxValue);
}
maxValue = ninf;
for (int i = n; i >= 1; i--) {
maxValue = max(maxValue, dp[i][j - 1]) + a[i][j];
dp[i][j] = max(dp[i][j], maxValue);
}
}
printf("%lld\n", dp[n][m]);
五、最后
这次比赛整体难度较低,第一题是二进制签到题,第二题暴力即可通过,第三题结合语法树与与或逻辑运算的短路特性,第四题是简单DP。
《完》
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号 ID:tiankonguse-code
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。