2023 睿抗机器人开发者大赛CAIP-编程技能赛-本科组(国赛)|赛题解析
前言
为了准备今年的比赛,做了下去年的题目。感觉这一套题还是比较难的,T2 题面意义不明卡了很久,T4 我用std::set然后MLE了,调了半天,证明是我比较菜。下面详细说一下我做这一套题的思路吧。
RC-u1 睿抗,启动!(15pts)
一个比较简单的模拟题,不解释。比较有意思的是,题干要求输入用户名为username,你要把这个字符串变成自己的名字。即便是一年之后的现在,我在赛场上刷题,也一样要遵循这个要求。因此每个人的代码在这个地方都不一样。
#include <bits/stdc++.h>
using namespace std;
string s,ss;int n;
string to_upper(string s) {
string ret = "";
for(const char &ch : s)
ret.append(1,ch - 'a' + 'A');
return ret;
}
string to_lower(string s) {
string ret = "";
for(const char &ch : s)
ret.append(1,ch - 'A' + 'a');
return ret;
}
string f(string s) {
string ret = "";
bool lower = false;
int cnt = 0;
for(int i = 0;i < s.size();i++)
if(s[i] >= 'a' and s[i] <= 'z') {
if(lower) cnt++;
else {
if(cnt >= 3)
ret += to_lower(s.substr(i - cnt,cnt));
else
ret += s.substr(i - cnt,cnt);
cnt = 1;
lower = true;
}
} else if(s[i] >= 'A' and s[i] <= 'Z') {
if(lower) {
if(cnt >= 3)
ret += to_upper(s.substr(i - cnt,cnt));
else
ret += s.substr(i - cnt,cnt);
cnt = 1;
lower = false;
} else cnt++;
} else {
if(cnt >= 3) {
if(lower) ret += to_upper(s.substr(i - cnt,cnt));
else ret += to_lower(s.substr(i - cnt,cnt));
} else
ret += s.substr(i - cnt,cnt);
cnt = 0;
ret.append(1,s[i]);
}
if(cnt >= 3) {
if(lower) ret += to_upper(s.substr(s.size() - cnt,cnt));
else ret += to_lower(s.substr(s.size() - cnt,cnt));
} else
ret += s.substr(s.size() - cnt,cnt);
return ret;
}
int main() {
cin >> n >> s;
if(s == "yourname")
s = "zhangsan"; // 这里写你的名字
ss = s;
while(n--) {
for(int i = 0;i < s.size();i++)
if(s[i] >= 'b' and s[i] <= 'z')
s[i]--;
else if(s[i] == 'a')
s[i] = 'z';
else if(s[i] >= 'A' and s[i] <= 'Y')
s[i]++;
else if(s[i] == 'Z')
s[i] = 'A';
s = f(s);
}
return cout << ss << '\n' << s,0;
}
RC-u2 桌游猜谜(20pts)
也是一个模拟题,但是不能特别暴力。题干很长,而且很多地方没说明白,这里重新解释一下。牌堆里面有 \(6\) 种不同颜色的卡牌,每种颜色有 \(8\) 张牌,分别写着数字 \(1-8\)。加上你一共有 \(n+1\) 个玩家,每个玩家手上分别拿着 \(6\) 张不同颜色的牌。你能看到其他人的牌色和数字,但是只能看到自己的牌色(等于什么也不知道了)。你可以选择三种不同的颜色,再选择一个范围 \([L,R]\),询问一次其他玩家,自己对应的三种颜色的卡牌的数字之和是不是在范围内。对于你的询问,其他玩家要么回答 yes,要么回答 no。
你想知道自己手上的牌数字是什么。如果其他玩家回答 yes,那么你可以排除一些可能,记这些可能数为 \(K_1\)。如果回到 no,你又可以排除 \(K_2\) 种情况。你的询问需要满足 \(\min(K_1,K_2)\) 最大。然后题目问你在这个询问的条件下,你手上的牌可能有多少种可能。这个地方我觉得题目没说明白,因为玩家回答 yes 或者 no,会导致我手上的牌有两种可能。如果回答 yes 排除的多一些,那么剩下的可能就少一些。这个题目其实是想问你两种可能种的较大值是多少。比如回答 yes,剩余 \(3\) 种可能,回答 no,剩余 \(2\) 种可能,那么答案就是 \(3\)。
模拟也很简单,在三种颜色确定下来后,我们可以得到自己手上这三种颜色的牌的所有可能情况。每一种情况对应一个和,这些和被记录在集合 \(S\) 里面。现在我们只需要确定 \([L,R]\),使得集合中落在区间 \([L,R]\) 内的个数和落在区间 \([L,R]\) 外的个数尽可能接近即可。
os:这题理解起来真的要命,跑出来的和样例结果不一样,样例还不给一个解释。
#include <bits/stdc++.h>
using namespace std;
// p[i][j] implies the card number of player i with color j
int t,n,p[10][6],poss[30],avai[6];
// vis[i][j] implies if the card of number j and color i is occupied
bool vis[6][9];
typedef struct {
int minval,res;
} solution;
vector<solution> ans;
const int inf = 0x3f3f3f3f;
void cal(int c1,int c2,int c3) {
int sumposs = 0;
memset(poss,0,sizeof(poss));
for(int i = 1;i <= 8;i++)
if(not vis[c1][i])
for(int j = 1;j <= 8;j++)
if(not vis[c2][j])
for(int k = 1;k <= 8;k++)
if(not vis[c3][k]) {
poss[i + j + k]++;
sumposs++;
}
int other_color = 1,minval = 0;
for(int i = 0;i < 6;i++)
other_color *= avai[i];
other_color /= avai[c1] * avai[c2] * avai[c3];
for(int i = 3;i <= 24;i++)
for(int j = i;j <= 24;j++) {
int temp = 0;
for(int k = i;k <= j;k++)
temp += poss[k];
int new_minval = min(temp,sumposs - temp);
if(new_minval > minval)
minval = new_minval;
}
ans.push_back(solution{minval * other_color,(sumposs - minval) * other_color});
}
int main() {
cin >> t;
while(t--) {
cin >> n;
memset(vis,0,sizeof(vis));
ans.clear();
for(int i = 0;i < 6;i++)
avai[i] = 8;
for(int i = 0;i < n;i++)
for(int j = 0;j < 6;j++) {
cin >> p[i][j];
vis[j][p[i][j]] = true;
avai[j]--;
}
for(int i = 0;i < 6;i++)
for(int j = i + 1;j < 6;j++)
for(int k = j + 1;k < 6;k++)
cal(i,j,k);
int idx = 0,minval = ans[0].minval;
for(int i = 1;i < ans.size();i++)
if(ans[i].minval > minval) {
minval = ans[i].minval;
idx = i;
}
cout << ans[idx].res << '\n';
}
return 0;
}
RC-u3 兰州拉面派餐系统(25 pts)
上面都是模拟题,从这里开始过渡到算法环节。我们需要维护一个数据结构,希望每次都能用比较小的代价,找到最早煮完面的、空闲且编号最小的篮子,以便处理下一单。这里我用的是set,实际上priority_queue(堆)也行。可以说priority_queue更好。不过我对set稍微熟悉一点,就直接拿来用了。
时间复杂度 \(O((l+m)\log m)\)。实际上永远有 \(m\leq l\),不然这题就不是这种做法了,因此时间复杂度 \(O(l\log m)\)。
#include <bits/stdc++.h>
using namespace std;
int n,m,l,cost[1005],ntype[100005],ans[10005];
vector<pair<int,int>> rec;
typedef struct basket_t{
int id,endtime,orderid;
bool operator<(const struct basket_t &other) const {
return endtime < other.endtime or endtime == other.endtime and \
id < other.id;
}
} basket;
set<basket> s;
bool kg;
int main() {
cin >> n >> m >> l;
for(int i = 1;i <= n;i++)
cin >> cost[i];
for(int i = 1;i <= l;i++)
cin >> ntype[i];
for(int i = 1;i <= m;i++)
s.insert(basket{i,0,0});
for(int i = 1;i <= l;i++) {
auto b = *s.begin();
s.erase(s.begin());
if(b.orderid) {
ans[b.id]++;
rec.push_back({b.orderid,b.endtime});
}
s.insert(basket{b.id,b.endtime + cost[ntype[i]],i});
}
while(not s.empty()) {
auto b = *s.begin();
s.erase(s.begin());
if(b.orderid) {
ans[b.id]++;
rec.push_back({b.orderid,b.endtime});
}
}
sort(rec.begin(),rec.end(),[&](const pair<int,int> &a,const pair<int,int> &b) -> bool {
return a.second < b.second or a.second == b.second and a.first < b.first;
});
assert(rec.size() == l);
for(int i = 0;i < l;i++)
cout << rec[i].first << ':' << rec[i].second << " \n"[i == l - 1];
for(int i = 1;i <= m;i++)
cout << ans[i ]<< " \n"[i == m];
return 0;
}
RC-u4 拆积木(30pts)
思路比较简单,对于编号为i的积木,维护一个press[i],表示紧贴着它压在上面的方块有press[i]个。一个积木i可以被拿出来,当且仅当press[i] == 0。所以大致流程就是:
-
把满足
press[i] == 0的积木加入到集合 \(S\)。 -
弹出集合 \(S\) 中编号最小的那个积木,将积木对应的方块移除。这会更改其他积木的
press值。一旦某个积木的press值变成0了,加入集合 \(S\)。 -
重复步骤 2 直到集合 \(S\) 为空。
这个时候我们查看是否所有积木都被移除了,如果不是,打出Impossible即可。时间复杂度是 \(O(nm\log(nm))\)。
这题主要是内存限制比较恶心,一个set,再加上一个 \(10^6\) 量级的vector<pair<int,int>>数组(存某个积木对应的方块位置),直接超64MB。主要是vector<pair<int,int>>如果为空也要占 24B,因此整个这个数组就是 24MB 了。可以简化为一个vector,里面的坐标按照积木 \(1\)、积木 \(2\)……这样排序,记录每个积木对应的左右端点即可。这个也调了我半天。
#include <bits/stdc++.h>
using namespace std;
int n,m,x[1000005],y[1000005],curjimu,target;
set<int> s;
int mp[1005][1005],press[1000005];
bool kg,vis[1005][1005],cx[1000005];
const int dx[] = {-1,0,0,1},dy[] = {0,-1,1,0};
void dfs(int xx,int yy) {
if(vis[xx][yy]) return;
vis[xx][yy] = true;
if(xx != n - 1 and mp[xx + 1][yy] != curjimu) {
int downblock = mp[xx + 1][yy];
if(--press[downblock] == 0)
s.insert(downblock);
}
for(int i = 0;i < 4;i++) {
int new_x = xx + dx[i],new_y = yy + dy[i];
if(mp[new_x][new_y] == curjimu)
dfs(new_x,new_y);
}
}
int main() {
cin >> n >> m;
for(int i = 0;i < n;i++)
for(int j = 0;j < m;j++) {
cin >> mp[i][j];
// blk.push_back({mp[i][j],{i,j}});
if(i and mp[i - 1][j] != mp[i][j])
press[mp[i][j]]++;
x[mp[i][j]] = i;
y[mp[i][j]] = j;
cx[mp[i][j]] = true;
}
for(int i = 0;i < 1000005;i++)
if(cx[i]) {
target++;
if(press[i] == 0)
s.insert(i);
}
while(not s.empty()) {
auto elem = *s.begin();
s.erase(s.begin());
target--;
if(kg) cout << ' ';
kg = true;
cout << elem;
curjimu = elem;
dfs(x[curjimu],y[curjimu]);
}
if(target) {
if(kg) cout << ' ';
cout << "Impossible";
}
return 0;
}
RC-u5 栈与数组(30pts)
这题容易想到二分,我们先锁定数组长度的一个可能的区间,然后通过二分法找到这个边界 \(l\),使得数组长度 \(<l\) 不行,\(\geq l\) 可以,那么得到数组长度的最小值为 \(l\)。关键是给定一个数组长度 \(l\),如何判断它可不可行?
可以用动态规划。设 \(dp[i][j]\) 表示从栈1顶部往下取 \(i\) 个元素,且从栈2顶部往下取 \(j\) 个元素,无视数组长度,最终数组中会有多少个位置被占用。比如 \(i=5,j=6\),从栈1取到了 \(2,2,3,3,3\),从栈2取到了 \(1,2,2,2,3\),\(k=3\)。那么最终这个数组会是 \(1,2,2,3\),因此 \(dp[5][6]=4\)。接下来我们记操作 \((i,j)\) 表示从栈1顶部往下取 \(i\) 个元素,且从栈2顶部往下取 \(j\) 个元素。记 \(ok[i][j]\) 表示操作 \((i,j)\) 在当前数组长度 \(len\) 下是否是可以完成的。显然,到达操作 \((i,j)\) 有两条可能的路径:
- 若最后一步取的是栈1的元素,那么 \((i,j)=(i-1,j)+(1,0)\)。
- 若最后一步取的是栈2的元素,那么 \((i,j)=(i,j-1)+(0,1)\)。
注意当 \(i=0\) 时,最多只有一条路径。\(j=0\) 同理。下面按照 \(i,j>0\) 有两条路径处理。
我们在详细讨论一下,两条路径是否真的能够到达 \((i,j)\)。首先,如果 \(dp[i][j] > len\),那么这个状态本身就是不可达的,\(ok[i][j]=0\)。否则,继续讨论:
- 若取 \((i,j)=(i-1,j)+(1,0)\),那么当 \(ok[i-1][j]=1\),且 \(dp[i-1][j]<len\) 时,是可以的,\(ok[i][j]=1\)。
- 若取 \((i,j)=(i,j-1)+(0,1)\),那么当 \(ok[i][j-1]=1\),且 \(dp[i][j-1]<len\) 时,是可以的,\(ok[i][j]=1\)。
- 如果上面两个路径都不行,那么 \(ok[i][j] =0\)。
其实就是一个动态规划的过程,时间复杂度为 \(O(tc_1(c_1+c_2)\log(c_1+c_2))\)。当然,可以优化为 \(O(tc_1c_2\log(c_1+c_2))\) 的代码。
#include <bits/stdc++.h>
int t,c1,c2,k;
int st1[1005],st2[1005];
int cnt[2005],sm,dp[1005][1005];
bool ok[1005][1005],vis[1005][1005];
using namespace std;
namespace wr{
void push(int x) {
sm++;
cnt[x]++;
if(cnt[x] >= k) {
cnt[x] -= k;
sm -= k;
}
}
}
bool cal(int p1,int p2,const int &len) {
if(p1 == 0 and p2 == 0) return true;
if(vis[p1][p2]) return ok[p1][p2];
vis[p1][p2] = true;
if(dp[p1][p2] > len)
return ok[p1][p2] = false;
if(p1 and cal(p1 - 1,p2,len) and dp[p1 - 1][p2] < len)
return ok[p1][p2] = true;
if(p2 and cal(p1,p2 - 1,len) and dp[p1][p2 - 1] < len)
return ok[p1][p2] = true;
return ok[p1][p2] = false;
}
bool is_ok(int len) {
memset(vis,0,sizeof(vis));
return cal(c1,c2,len);
}
int main() {
cin >> t;
while(t--) {
cin >> c1 >> c2 >> k;
for(int i = 1;i <= c1;i++)
cin >> st1[i];
for(int i = 1;i <= c2;i++)
cin >> st2[i];
for(int i = 0;i <= c1;i++) {
memset(cnt,0,sizeof(cnt));
sm = 0;
for(int j = 1;j <= i;j++)
wr::push(st1[j]);
dp[i][0] = sm;
for(int j = 1;j <= c2;j++) {
wr::push(st2[j]);
dp[i][j] = sm;
}
}
int l = 0,r = c1 + c2;
while(r - l > 1) {
int mid = (l + r) >> 1;
if(is_ok(mid))
r = mid;
else l = mid;
}
cout << r << '\n';
}
}

浙公网安备 33010602011771号