The 2023 ICPC Asia Jinan Regional Contest (The 2nd Universal Cup. Stage 17: Jinan)补题记录
The 2023 ICPC Asia Jinan Regional Contest (The 2nd Universal Cup. Stage 17: Jinan)
D. Largest Digit
题意:给定两个范围la, ra, lb, rb,求在两个范围内选任意两个数相加,求最大的数位
思路:暴力枚举即可,遇到9跳出循环
void solve(){ ll la, ra, lb, rb; cin >> la >> ra >> lb >> rb; ll ans = 0; for(ll i = la; i <= ra; i ++){ for(ll j = lb; j <= rb; j ++){ ll res = 0; ll u = i + j; while(u){ res = max(res, u % 10); u /= 10; } ans = max(ans, res); if(ans == 9) break; } if(ans == 9) break; } cout << ans << '\n'; }
I. Strange Sorting
题意:给定一个长度为 n 的排列,可以至多选择⌊n / 2⌋个(l, r),并且 al < ar ,将(l, r)范围内的数字按照升序排序,求将整个排列完成升序排序的一个方案
思路:每次找当前第一个 a[i]!=i 的数字然后找最后的 a[j]<a[i] 的数字,对这个区间进行排序,在给定的范围内能完成排序
void solve(){ int n; cin >> n; vector<int> a(n + 1); for(int i = 1; i <= n; i ++) cin >> a[i]; vector<PII> ans; for(int i = 1; i <= n; i ++){ if(a[i] != i){ int p = i; for(int j = i + 1; j <= n; j ++){ if(a[j] < a[i]) p = j; } sort(a.begin() + i, a.begin() + p + 1); ans.push_back({i, p}); } } cout << ans.size() << '\n'; for(auto [x, y] : ans){ cout << x << ' ' << y << '\n'; } }
A. Many Many Heads
题意:给定一个括号序列,包含[]()这四种,你可以对括号进行左右变化任意次,但是不能改变括号的种类,问是否存在且仅存在一种合法的括号序列
思路:赛时和队友想的做法是:先把当前的括号序列进行构造变成一个合法的括号序列,然后进行判断每对括号内是否存在两对相同类型的括号序列在同一深度
深度的定义:例如这样的括号序列 ([])([]),我们对他们进行编号:1,2,3,4,5,6,7,8,那么1,4,5,8的括号位于0深度,2,3,6,7括号的深度为1
如果存在同样深度的两对括号,例如(.......)(......)那么这两对括号一定能够组成另外一种合法的情况
那么为什么一定会出现这种情况呢?这和一开始我们构造的方法有关系:我们定义一个存储符号的数组 a,当a为空,我们把第一个放入的括号都变成向右的括号,然后检测之后加入的括号类型,如果是类型相同的括号就配对为一对,否则加入对应类型的向右的括号,所以同一层内只要合法是不会出现(())这种的情况
然后接下来就是:对深度进行检测,如果当前同一深度的不存在相同的,就把这一层清空,因为后面有可能还会进入这个深度
void solve(){ string s, s1; cin >> s; vector<char> a; for(int i = 0; i < s.size(); i ++){ if(a.size() == 0){ if(s[i] == '(' || s[i] == ')'){ a.push_back('('); s1 += '('; } else{ a.push_back('['); s1 += '['; } }//空先放左括号 else{ if(a.back() == '('){ if(s[i] == '(' || s[i] == ')'){ s1 += ")"; a.pop_back(); } else{ a.push_back('['); s1 += '['; } } else if(a.back() == '['){ if(s[i] == '[' || s[i] == ']'){ s1 += "]"; a.pop_back(); } else{ a.push_back('('); s1 += '('; } } }//对应匹配 } int p = 0, q = 0;//记录当前深度 map<int, int> x, y; //x是圆括号的深度,y是方括号的深度 for(auto c:s1) { if(c == '(') p ++; else if(c == '[') q ++;//左边界出现,当前深度++ else if(c == ')'){ x[q] ++; if(x[q] >= 2){ cout << "No\n"; return; }//对应深度出现了两对 y[p] = 0;//这个深度合法就要清空 p --;//回到上一个深度 } else if(c == ']'){ y[p] ++; if(y[p] >= 2){ cout << "No\n"; return; } x[q] = 0; q --; } } cout << "Yes\n"; }
补题:正解很简单,判断三个连续相同的和两个以上长度大于等于2的连续段会造成多种结果即可
void solve() { string s; cin >> s; int a = 0; int n = s.size(); for(int i = 0; i < n; i ++){ if(s[i] == ')') s[i] = '('; if(s[i] == ']') s[i] = '['; } for(int i = 0; i < n; i ++){ if(s[i + 1] == s[i]){ int res = 1; while(s[i + 1] == s[i]){ i ++; res ++; } if(res >= 3){ cout << "No\n"; return; } else a ++; } } if(a >= 3) cout << "No\n"; else cout << "Yes\n"; }
G. Gifts from Knowledge
题意:给定一个01矩阵,可以选择将每一行是否进行左右翻转,求让每一列最多有一个1的方案数
思路:首先是答案肯定是0的情况:如果对于第 i 列和第 n - i + 1 列的 1 的数量之和大于2,那么肯定是无法做到每一列至多一个 1 ,方案数就是0
然后看如何翻转:
1:如果第 i 列和第 n - i + 1 列的 1 的数量之和小于等于 1 那么这 个 1 所在的一行翻转和不翻转都可以
2:如果第 i 列和第 n - i + 1 列的 1 的数量之和为2
(1):如果这两个 1 在同一列,那么这两行(指的是这两个1所在的行)必须翻转一个
(2):如果这两个 1 不在同一列,那么这两行(同上)必须要不都翻转,要不都不翻转
接着如何实现:
我们使用一个互斥并查集:我们定义 i 为翻转这一行,i + n 为不翻转这一行
我们定义 i1 ,i2 分别是 2 中两个 1 所在的行
对于 2.(1):我们将 (i1, i2 + n)以及 (i1 + n, i2)这两对点合并
对于 2.(2):我们将 (i1, i2)以及 (i1 + n, i2 + n)这两对点合并
最后如果存在(i, i + n)在一个联通块,那就是互斥了,方案数是0
否则答案就是2连通块数量 / 2
ll fastpow(ll a, ll n){ ll base = a, res = 1; while(n){ if(n & 1) res = (res * base) % Mod; base = (base * base) % Mod; n >>= 1; } return res; } void solve(){ ll n, m; cin >> n >> m; vector<string> b(n); for(auto & s : b) cin >> s; vector<int> p(n * 2); for(int i = 0; i < 2 * n; i ++) p[i] = i; auto find =[&] (auto && find, int x) -> int{ if(x != p[x]) p[x] = find(find, p[x]); return p[x]; }; auto add =[&] (int x, int y) -> void{ p[find(find, x)] = find(find, y); }; for(int i = 0; i < m; i ++){ int sum = 0; for(int j = 0; j < n; j ++) sum += (b[j][i] == '1'); if(sum >= 3){ cout << 0 << '\n'; return; } } for(int l = 0, r = m - 1; l <= r; l ++, r --){ vector<int> l1, r1; for(int i = 0; i < n; i ++) if(b[i][l] == '1') l1.push_back(i); for(int i = 0; i < n; i ++) if(b[i][r] == '1') r1.push_back(i); if(l1.size() == 2) add(l1[0], l1[1] + n), add(l1[0] + n, l1[1]); if(r1.size() == 2) add(r1[0], r1[1] + n), add(r1[0] + n, r1[1]); for(auto v : l1){ for(auto w : r1){ add(v, w); add(v + n, w + n); } } } for(int i = 0; i < n; i ++){ if(p[find(find, i)] == p[find(find, i + n)]){ cout << 0 << '\n'; return; } } int ans = 0; for(int i = 0; i < 2 * n; i ++) ans += (p[i] == i); ans = fastpow(2, ans / 2); cout << ans << '\n'; }
K. Rainbow Subarray
题意:定义一个数组的连续子数组(i - n - 1)每一项满足 ai + 1 - ai = 1 为一个彩虹子数组,可以至多进行 k 次操作,求能构造出的最长彩虹子数组
思路:考虑ai+1 - ai = 1 可以转化为 ai+1 - (i + 1) = ai - i ,所以先预处理把所有的ai - i ,问题就变成了:把一个区间的所有数字全部变成一个相同的数字,很容易知道,最小代价就是把所有数变成中位数即可,所以接下来就用一个滑动窗口来维护,求最大的区间长度,这里动态维护滑动窗口的中位数用到了对顶堆的技巧
实现:
我们开两个 multiset s1, s2来分别维护大于中位数和小于中位数的数
先来思考查询的方式:
1:如果当前窗口内的元素数量之和是偶数,那么中位数就是(s1.rbegin() + s2.begin()) / 2
2:如果当前窗口内的元素数量之和是奇数,那么中位数就是s2.begin()(后面讲述原因)
再来考虑插入的方式:
1:如果两个 multiset 都为空,那么就优先往 s2 里面插入元素
2:如果当前元素大于等于 s2.begin(),那就把元素插入 s2,否则插入 s1
由于 s2.size() 始终大于等于 s1.size() 所以奇数情况的中位数就是 s2.begin()
我们要使得中位数符合我们的预期,那么两个 multiset 的元素数量之差不能超过1,所以我们需要一个 balance操作,使得两个 multiset 平衡
1:如果 s2 的元素数量大于 s1 的元素数量+1,那就把 s2.begin() 转移到 s1 中
2:如果 s1 的元素数量大于 s2 的元素数量,那就把 s1.rbegin() 转移到 s2 中(注意不要忘了从原 multiset 删除)
为什么会出现 s1 的元素数量大于 s2 ?
因为在滑动窗口滑动的过程中,我们删除左端点的时候无法确定是在 s1 还是 s2 中
最后就是如何计算滑动窗口中元素改成中位数的贡献了
我们用两个变量 sum1 ,sum2 分别记录 s1,s2 的元素值之和,那么贡献就是 (s1.size() * mid) - sum1 + sum2 - (s2.size() * mid)
因为 s1 中 都是小于中位数 mid 的数字,s2 中都是大于等于中位数 mid 的数字
void solve(){ ll n, k; cin >> n >> k; vector<ll> a(n + 1); for(int i = 1; i <= n; i ++){ cin >> a[i]; a[i] -= i; } multiset<ll> s1, s2; int l = 1, r = 1; ll ans = 0; ll sum1 = 0, sum2 = 0; auto balance =[&]() -> void{ ll siz1 = s1.size(), siz2 = s2.size(); if(siz2 > siz1 + 1){ ll now = *s2.begin(); s1.insert(now); sum1 += now; sum2 -= now; s2.erase(s2.begin()); } else if(siz1 > siz2){ ll now = *s1.rbegin(); s2.insert(now); sum2 += now; sum1 -= now; s1.erase(prev(s1.end())); } }; auto insert =[&](ll x) -> void{ if(s2.empty()) s2.insert(x), sum2 += x; else{ int num = *s2.begin(); if(x >= num){ s2.insert(x); sum2 += x; } else{ s1.insert(x); sum1 += x; } } balance(); }; auto query =[&]() -> ll{ ll siz = s1.size() + s2.size(); if(siz & 1) return *s2.begin(); else return (*s1.rbegin() + *s2.begin()) / 2; }; auto erase =[&](ll x) -> void{ if(s1.find(x) != s1.end()) s1.erase(s1.find(x)), sum1 -= x; else s2.erase(s2.find(x)), sum2 -= x; balance(); }; auto ques =[&]() -> ll{ ll mid = query(); ll ned = mid * s1.size() - sum1 + sum2 - mid * s2.size(); if(ned > k) return false; else return true; }; for(l = 1, r = 1; r <= n; r ++){ insert(a[r]); while(l <= r && !ques()){ erase(a[l]); l ++; } ll siz = s1.size() + s2.size(); ans = max(ans, siz); } cout << ans << '\n'; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现