2025年5月月赛 乙组T1~T3
目录
T1 逆序对数
T2 平衡 01 串
T3 城市漫步
总结
T1 逆序对数
上海市计算机学会竞赛平台 | YACS
p为n的排列,没有重复元素。
p的子序列要和p的逆序对数相等,则该子序列要包含p的所有逆序对的数字,其他数字可包含可不包含。
设k = 不是逆序对的数字个数,则符合要求的p的子序列有(2^k) mod Mo 个,若k = n,则符合要求的p的子序列有(2^k - 1) mod Mo个(不包含空子序列)。
对p归并排序,在归并排序的过程中标记逆序对。
代码如下:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;typedef long long LL;const LL Maxn = 1e5 + 5;
const LL Mo = 998244353;bool vis[Maxn];void f_qsort(LL left, LL mid, LL right, vector<LL>& vct) {vector<LL> v_L(vct.begin() + left, vct.begin() + mid + 1);vector<LL> v_R(vct.begin() + mid + 1, vct.begin() + right + 1);bool flag = false;for (LL k = left, i = 0, j = 0; k <= right; ++k) {if (j >= v_R.size() && i < v_L.size()) {vct[k] = v_L[i++];} else if (i >= v_L.size() && j < v_R.size()) {vct[k] = v_R[j++];} else if (v_L[i] > v_R[j]) {vis[v_R[j]] = true;if (flag == false) {for (LL s = i; s < v_L.size(); ++s) {vis[v_L[s]] = true;flag = true;}}vct[k] = v_R[j++];} else if (v_L[i] < v_R[j]) {vct[k] = v_L[i++];}}
}void f_zsort(LL left, LL right, vector<LL>& vct) {if (left >= right) return;LL mid = left + ((right - left) >> 1);f_zsort(left, mid, vct);f_zsort(mid + 1, right, vct);f_qsort(left, mid, right, vct);
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);LL t, n, cnt = 0, res = 1;cin >> t;while (t--) {cin >> n;vector<LL> vct(n, 0);for (LL i = 0; i < n; ++i) {cin >> vct[i];vis[vct[i]] = false;}f_zsort(0, n - 1, vct);cnt = 0;res = 1;for (LL i = 0; i < n; ++i) {if (vis[vct[i]] == false) {res = (res * 2) % Mo;++cnt;}}if (cnt < n) cout << res << '\n';else cout << res - 1 << '\n';}return 0;
}
在f_qsort函数的for循环里重复标记会超时。
前面的遍历只要发现了逆序对,设前面遍历到v_L的i位置,v_R的j位置,则[v_L[i], v_L[v_L.size() - 1]]和v_R[j]被标记,当前发现逆序对设当前遍历到v_L的i’位置,v_R的j’位置,则[v_L[i’], v_L[v_L.size() - 1]]和v_R[j’]被标记,i < i'重复标记了,所以只在第一次发现逆序对时标记v_L。原理如下图:
当v_R[j] < v_L[i]时出现逆序对,因为v_L数组有序,则i之后的元素也和v_R[j]组成逆序对。
T2 平衡 01 串
上海市计算机学会竞赛平台 | YACS
答案具有单调性且能在O(n)时间内判定,用二分答案。
最优解:最小权重
判定函数:
- 思路 贪心,区间[start + 1, last - 1]外的1个数 = mid,不断调整start、last看区间[start + 1, last - 1]是否有[start + 1, last - 1]内的0 <= mid个的情况,若有返回true,尝试所有依旧不存在0 <= mid返回false。
- 实现 双指针法,开始start移至[0, start]1的个数 = mid,last移到[last, str.size() - 1]全为0,,判断权值是否 <= mid,之后循环mid次,每次start移到前面一个为1的位置([start + 1, last - 1]包含进1个1),last移到前一个为1的位置之后([start + 1, last - 1]退出1个1),然后判断权值是否 <= mid。该函数时间复杂度约为O(n)。
代码如下:
#include <iostream>
#include <string>
#include <cmath>
using namespace std;typedef long long LL;const LL Maxn = 2 * 1e5 + 5;LL vct[Maxn];void init(LL n) {for (LL i = 0; i <= n; ++i) vct[i] = 0;
}bool f_check(LL mid, LL len, string& str) {LL start = 0, last = len - 1, cnt = 0;while (start < len) {if (str[start] == '1') {if (cnt >= mid) break;++cnt;++start;} else {++start;}}--start;while (last >= 0 && str[last] == '0') --last;++last;if (start > last - 1) return true;if (start == -1 && vct[last - 1] <= mid) return true;if (start != -1 && vct[last - 1] - vct[start] <= mid) return true;for (LL s = 1; s <= cnt; ++s) {while (start >= 0 && str[start] == '0') --start;--start;last -= 2;while (last >= 0 && str[last] == '0') --last;++last;if (start > last - 1) return true;if (start == -1 && vct[last - 1] <= mid) return true;if (start != -1 && vct[last - 1] - vct[start] <= mid) return true;}return false;
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);LL t, scnt1 = 0, L = 0, R = 0, mid = 0;string str;cin >> t;while (t--) {cin >> str;init(str.size());scnt1 = 0;for (LL i = 0; i < str.size(); ++i) {if (i > 0) vct[i] = vct[i - 1];if (str[i] == '0') ++vct[i];else ++scnt1;}L = 0, R = scnt1;while (L < R) {mid = ((L + R) >> 1);if (f_check(mid, str.size(), str) != false) R = mid;else L = mid + 1;}cout << L << '\n';}return 0;
}
改变区间左右端点可用双指针,暴力两层for循环枚举区间也可看作双指针。注意指针初始化和while循环结束后的指针状态及指针的偏移量。
T3 城市漫步
上海市计算机学会竞赛平台 | YACS
树(图是树加上若干环和重边)由点和边组成,这道题考虑点比较复杂,那么考虑边。树上两点间只有一条路径,该路径是简单路径。
先假设y = x,贪心,只考虑必须经过的边(到达ai路径上的边)这条边是不能省的,遇到这种边sum += 2(包括回去路径)。
考虑y,两种情况,设c,d间的路径长度为dis(c, d)
- y在到ai的路径上,sum -= dis(x, y),不用再回x
- y不在到ai的路径上,贪心,sum加上的尽可能少。回到x后再走到y是sum += dis(x, y),设能到达y(y的父节点)且是经过点的为t,不再从t走到x,从t直接到y,sum = sum - dis(x, t) + dis(t, y),可看出t离y越近sum减的越多,dis(t,y) < dis(x, y),sum = sum - dis(x, t) + dis(t, y)方案最优。
第一种情况是第二种情况的特殊情况。
分析可得,需求每个节点到根节点的距离、y的父节点到y的距离,是x到ai的路径上的点要标记。
代码如下:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;typedef long long LL;const LL Maxk = 2 * 1e5 + 5;
const LL Maxn = 2 * 1e5 + 5;
const LL MVal = 1e14;vector<LL> grid[Maxn];
LL Dis[Maxn], visy[Maxn], idx = 0;
bool visk[Maxn];void init(LL n) {for (LL i = 0; i <= n; ++i) {visk[i] = false;visy[i] = MVal;Dis[i] = 0;grid[i].clear();}
}LL dfs(LL u, LL fa) {LL sum = 0;for (auto v : grid[u]) {if (v == fa) continue;Dis[v] = Dis[u] + 1;sum += dfs(v, u);}if (sum > 0) visk[u] = true;if (visy[u] < MVal) {if (visk[u] != false && idx == 0) idx = u;visy[fa] = visy[u] + 1;}if (sum > 0 || visk[u] != false) return sum + 2;else return 0;
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);LL t, n, k, x, y, u, v, sum = 0;cin >> t;while (t--) {cin >> n >> k >> x >> y;init(n);visy[y] = 0;visk[x] = true;for (LL i = 1; i <= k; ++i) {cin >> u;visk[u] = true;}for (LL i = 1; i < n; ++i) {cin >> u >> v;grid[u].push_back(v);grid[v].push_back(u);}idx = 0;sum = dfs(x, 0) - 2;cout << (sum + visy[idx] - Dis[idx]) << '\n';}return 0;
}
一遍dfs即可,对于无根树,dfs函数的参数直接明确父子节点,直接求visy即可。
能一次dfs的不要多次dfs,递归费栈空间。
总结
1.先想暴力法,再优化
- 可二分答案或查找且能O(n)时间内判定,用二分
- 要取最大优先级元素,用堆(priority_queue)优化
- 区间合并、查找等用线段树优化
- dfs有递推关系的,用动规,只要有阶段、状态、状态转移就用动规
- 枚举区间端点看是否能用双指针,双指针可看做多个变量同时工作,可提高效率。
2.对于图的问题点难考虑,尝试用边考虑。
3.看到排列、元素不重复、限制条件等信息不要忽略,可能是突破口,求方案数的题不一定要硬算方案数
4.逻辑写清楚比少写几行代码更重要