沈阳化工大学第十一届程序设计竞赛专业组题解
沈阳化工大学第十一届程序设计竞赛专业组题解
题目难度排序:
⭐
⭐⭐
⭐⭐⭐
⭐⭐⭐⭐
⭐⭐⭐⭐⭐
A. 转"人工"
根据题意模拟即可。
#include <iostream> using namespace std; int main() { string s; cin >> s; cout << s.back() << "模式"; return 0; }
B.马弓手关羽请战华雄
考虑倒着做。
用cnt来记录当前走了多少区块,每当 , cnt便可重新置0。
最后看cnt 是否大于0即可。
#include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; i++) { cin >> a[i]; } int cnt = 0; for (int i = n - 1; i >= 1; i--) { cnt++; if (a[i] >= cnt) cnt = 0; } cout << (cnt ? "NO" : "YES"); return 0; }
C.整整齐齐
思路:贪心+思维
先对左端点进行排序,枚举左端点,每次优先处理
最小的书,一直向后放置,直到下一个
的时候停止, 这个过程我们可以用一个小根堆进行维护;
即:
先对左端点排序,然后把同一个左端点的右端点都丢进去小根堆,然后尽可能多的放置右端点更小的书,若没有放置完的,保留在小根堆里等待下一个左端点。
因为是在下一个左端点处理上一个左端点,所以我们要在最后加入一个inf来处理最后一个左端点。
#include <iostream> #include <algorithm> #include <vector> #include <queue> #define int long long using namespace std; using PII = pair<int, int>; const int inf = 2e9 + 1; void solve() { int n; cin >> n; vector<PII> v(n); for(int i = 0; i < n; i ++) { cin >> v[i].first >> v[i].second; } sort(v.begin(), v.end()); v.push_back({inf, inf}); priority_queue<int, vector<int>, greater<int> > q; int ok = 1; int pos = -1; for(auto [l, r] : v) { if(pos == l) { q.push(r); } else { while(pos < l && q.size()) { auto t = q.top(); q.pop(); if(pos <= t){ pos ++; } else { ok = 0; } } pos = l; q.push(r); } } if(ok) { cout << "Yes\n"; } else { cout << "No\n"; } return; } signed main() { int T = 1; cin >> T; while(T --){ solve(); } return 0; }
D.凿冰 Action
最优决策问题。
可以发现小A和小B永远按照最优决策来选择冰块。
显然两人会采取这样的策略:对于每一根冰柱,保护属于自己容易取到的那一半不被对方取走。
即:谁都不放弃自己的价值高的冰块,就导致小A和小B能且只能拿到离自己近的冰块
Last:
对于有偶数个冰块的冰柱:小A、小B各拿一半,答案加上这些冰块的价值即可
那么如果冰块数是奇数的冰柱,那么中间那一块,就会陷入两难的境地。
所以我们考虑把中间的冰块价值差入一个大根堆中,两人轮流取最大值,这样就能保证两人所得分数最大。
#include <bits/stdc++.h> #define int long long using namespace std; signed main() { int n; cin >> n; int ans1 = 0, ans2 = 0; priority_queue<int> q; // 大根堆 for (int i = 0; i < n; i ++ ) { int k ; cin >> k; if (k % 2 == 0) { // 对半取 for (int j = 1, x; j <= k; j ++) { cin >> x; if (j > (k / 2)) { ans2 += x; } else { ans1 += x; } } } else { //中间的插入堆中 for (int j = 1, x; j <= k; j ++ ) { cin >> x; if (j == (k / 2 + 1)) { q.push(x); } else if (j <= k / 2) { ans1 += x; } else { ans2 += x; } } } } int ok = 0; while (q.size()) { if (!ok) { ans1 += q.top(); q.pop(); ok ^= 1; } else { ans2 += q.top(); q.pop(); ok ^= 1; } } cout << ans1 << " " << ans2 << "\n"; return 0; }
E.城中分支
首先发现序列数不多,不超过300个,也不超过300,可以想到300以内质数的数量一定很少,不超过70个;
- 区间欧拉函数就是区间积,很明显可以用线段树维护 。把欧拉函数看成两部分相乘,一个是
本身,一个是
包含的所有质因子项的乘积,我们线段树维护的节点有两个信息,一个是区间乘积,一个是区间包含的质因子,后者用状压,用一个longlong的变量保存。
- 质因子项我们可以预处理+逆元得到,用
表示第i个质数
的
;
#include<bits/stdc++.h> using namespace std; using i64 = long long; using PII = pair<i64, i64>; #define lc rt << 1 #define rc rt << 1 | 1 const int MAXN = 4e5 + 10; const int maxx = 40; const int mod = 1e9 + 7; //给定序列 初始值不超过300 区间乘 查询区间积的欧拉函数 //维护区间积以及区间积的质因子状态 //用线段树 欧拉函数根据公式计算 由于涉及取模 需要记录质因子 预处理每个质因子乘积项 i64 tree[MAXN << 2],has[MAXN << 2]; //区间积 区间质因子状态 i64 tag1[MAXN << 2],tag2[MAXN << 2]; //乘积的懒标记 乘积质因子懒标记 i64 sta[305]; //数i的质因子压缩状态 右边第一位是第一个质数2 int pri[65], tot = 0; //质数表 bool vis[305]; i64 f[65]; //预处理质因子项 i64 inv[305]; //逆元 void init() { for (int i = 2; i <= 300; i ++) { if (!vis[i]) pri[++ tot] = i; for (int j = 1;j <= tot && i * pri[j] <= 300; j ++) { vis[i * pri[j]] = 1; if(i % pri[j] == 0) break; } } for (int i = 2; i <= 300; i ++) { //i的每个质因子都压缩成二进制状态 右边第一个位置对应质数2 for(int j = 1; j <= tot; j ++) { if(i % pri[j] == 0) sta[i] |= 1LL << (j - 1); //注意LL } } inv[1] = 1; for (int i = 2; i <= 300; i ++) { inv[i] = mod - 1LL * (mod / i) * inv[mod % i] % mod; } for (int i = 1; i <= tot; i ++) { f[i] = inv[pri[i]] * (pri[i] - 1) % mod; //预处理 方便计算欧拉函数 } } void pushup(int rt) { //合并区间信息 tree[rt] = tree[lc] * tree[rc] % mod; has[rt] = has[lc] | has[rc]; } void build(int rt,int l,int r) { tag1[rt] = 1; //乘标记 tag2[rt] = 0; //质因子标记 这个区间乘的数包括哪些质因子 状压 if(l == r) { scanf("%d", &tree[rt]); has[rt] = sta[tree[rt]]; return; } int mid = l + r >> 1; build(lc, l, mid); build(rc, mid + 1, r); pushup(rt); } i64 qpow(i64 a,int b) { i64 ans = 1; while (b) { if (b & 1) { ans = ans * a % mod; } a = a * a % mod; b >>= 1; } return ans; } void change(int rt, int len, i64 v, i64 y) { //区间每个数乘v 更新区间乘积以及质因子状态 tree[rt] = qpow(v, len) * tree[rt] % mod; has[rt] |= y; tag1[rt] = tag1[rt] * v % mod; tag2[rt] |= y; } void pushdown(int rt,int l, int r) { if(!tag2[rt]) return; //这个区间没有乘过 int mid = l + r >> 1; change(lc, mid - l + 1, tag1[rt], tag2[rt]); change(rc, r - mid, tag1[rt], tag2[rt]); //下传完 标记初始化 tag1[rt] = 1; tag2[rt] = 0; } void updata(int rt, int l, int r, int v, int vl, int vr) { //[vl,vr]每个数乘v if(vl > r || vr < l ) return; if(vl <= l && r <= vr) { change(rt,r - l + 1, v, sta[v]); return; } int mid = l + r >> 1; pushdown(rt, l, r); updata(lc, l, mid, v, vl, vr); updata(rc, mid + 1, r, v, vl, vr); pushup(rt); } PII hb(PII a, PII b) { a.first = a.first * b.first % mod; a.second |= b.second; return a; } //返回[vl,vr]区间积以及质因子状态 PII query(int rt,int l,int r,int vl,int vr) { if(vl <= l && r <= vr) { return make_pair(tree[rt], has[rt]); } int mid = l + r >> 1; pushdown(rt, l, r); if(vr <= mid) return query(lc, l, mid, vl, vr); else if(vl > mid) return query(rc, mid + 1, r, vl, vr); return hb(query(lc, l, mid, vl, vr), query(rc, mid + 1, r, vl, vr)); } int n, m; int main() { init(); scanf("%d%d", &n, &m); build(1, 1, n); char op[15]; int l, r, v; while(m --) { scanf("%s%d%d",op, &l, &r); if(op[0] == 'Q') { PII t = query(1, 1, n, l, r); for (int i = 1; i <= tot; i ++) { if(t.second & (1LL << (i - 1))) t.first = (t.first * f[i]) % mod; } printf("%lld\n", t.first); } else { scanf("%d", &v); updata(1, 1, n, v, l, r); } } return 0; }
F.寻宝
首先解决第一个问题,如何求最长回文子串?
可以发现,字符串很长,想到解决这个问题,我们需要一个算法:马拉车算法。
使用马拉车后,我们便可以求出的大小。
然后,我们先考虑如果在数组中选择不超过个宝物的最大价值。
表示:以
为右端点,长度不超过
的连续子区间的最大总和
那么易知: 其中
表示前缀和
观察转移方程: 是常量,则
因此发现我们需要 动态去找一个区间中的最小值,我们便考虑滑动窗口维护DP
最后期望只需要 即可 注意取模。
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e6 + 10, mod = 998244353; int a[N], sum[N]; struct Manacher { // 马拉车算法 string s; Manacher(string str) { s = str; } int get_len() { // 获得最长回文子串 string b; b += "$#"; for (int i = 0; i < s.size(); i ++) { b += s[i]; b += '#'; } b += '^'; int len = b.size(); vector<int> p(len + 1); int mr = 0, mid = 0; for (int i = 0; i < len; i ++) { if (i < mr) { p[i] = min(p[2 * mid - i], mr - i); } else { p[i] = 1; } while (b[i - p[i]] == b[i + p[i]]) { p[i] ++; } if (i + p[i] > mr) { mr = i + p[i]; mid = i; } } int res = 0; for(int i = 0; i < len; i ++) { res = max(res, p[i]); } return res - 1; } }; int qmi(int a, int b) { int res = 1; while (b) { if (b & 1) { res = res * a % mod; } a = a * a % mod; b >>= 1; } return res; } signed main() { int len, n, k; cin >> len >> n >> k; string s; cin >> s; Manacher str(s); // 马拉车算法 int m = str.get_len(); for (int i = 1; i <= n; i ++) { cin >> a[i]; sum[i] = (sum[i - 1] + a[i]); // 前缀和 } vector<int> dp(n + 1, -1e18); // dp deque<int> q; q.push_back(0); for (int i = 1; i <= n; i ++) { // 滑动窗口 while (q.size() && i - m > q[0]) { q.pop_front(); } dp[i] = max(dp[i], sum[i] - sum[q[0]]); while (q.size() && sum[q.back()] >= sum[i]) { q.pop_back(); } q.push_back(i); } int ans = -1e18; for (int i = 1; i <= n; i ++) { ans = max(ans, dp[i]); } ans = max(ans, 0ll); cout << ans * qmi(k, mod - 2) % mod << "\n"; return 0; }
G.寻宝II
考点:圆方树
首先要求找到 的任意简单路径。对于经过的每一个点双联通分量,如果
在此点双内,则必然存在
的简单路径;
如果不属于任一经过的点双,则不可能存在
的简单路径;
因此,我们可以使用算法构造原图的圆方树
来解决此问题。
则:
在上找到
的唯一简单路径。对于每一个方点,如果
是与其相邻的圆点,则必然存在
的简单路径;
如果不与任一经过的方点相邻,则不可能存在
的简单路径;
#include <iostream> #include <vector> using namespace std; const int MAXN = 2e5 + 10; void chmin(int& x, int y) { if(y < x) x = y; } vector<int> edge[MAXN], T[MAXN << 1]; void add_edge(vector<int>* V, int x, int y) { V[x].push_back(y); V[y].push_back(x); } int dfc, dfn[MAXN], low[MAXN], top, st[MAXN], cnt; void tarjan(int v) { // 构造圆方树 low[v] = dfn[v] = ++ dfc; st[++ top] = v; for(int u: edge[v]) { if(!dfn[u]) { tarjan(u); chmin(low[v], low[u]); if(low[u] == dfn[v]) { add_edge(T, v, ++cnt); do { add_edge(T, st[top], cnt); } while(st[top--] != u); } } else chmin(low[v], dfn[u]); } } int n, m, a, b, c, ct[MAXN << 1]; void dfs(int v, int par) { if(v > n) { for(int u: T[v]) { ct[u] ++; } } if(v == c) { cout << (ct[b] ? "Yes\n" : "No\n"); exit(0); } for(int u: T[v]) { if(u != par) { dfs(u, v); } } if(v > n) { for(int u: T[v]) { ct[u] --; } } } int main() { cin >> n >> m >> a >> b >> c; while (m --) { int x, y; cin >> x >> y; add_edge(edge, x, y); } cnt = n; tarjan(1); dfs(a, -1); return 0; }
H.关键学生
考点:并查集。
可以发现,如果按顺序暴力维护每次老师抓走后学生中帮派数,会超时。
我们考虑在所有具有关系的同学对中,所有会被老师抓走的学生先不进行合并,那么最后帮派的数量就是老师抓走所有人之后的帮派数量。
然后我们倒着做,每次将与当前抓走学生有关系的所以学生进行合并,维护并查集,从而便可以快速得到帮派数量。
tip:将所有姓名用数字表示便可以使用普通并查集, 也可以使用字符串并查集。
#include <bits/stdc++.h> using namespace std; using i64 = long long; using PII = pair<string, string>; constexpr int N = 4e5 + 10; int n, m; map<string, string> p; // 字符串并查集 map<string, vector<string>> g; // 存放跟某个学生有关系的所有学生 string find(string x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> m; for (int i = 1; i <= n; i ++) { string s; cin >> s; p[s] = s; } vector<PII> adj; // 存放所有学生关系对 for (int i = 0; i < m; i ++) { string a, b; cin >> a >> b; adj.push_back({a, b}); g[a].push_back(b); g[b].push_back(a); } int k; cin >> k; vector<string> op(N); // 存放被抓学生姓名 map<string, int> vis; // 存放学生是否被抓 for (int i = 1; i <= k; i ++) { cin >> op[i]; vis[op[i]] = 1; } int ans = n - k; for (auto [a, b] : adj) { // 将没有被抓的学生进行合并 if (vis.count(a) || vis.count(b)) { continue; } string pa = find(a), pb = find(b); if (pa != pb) { p[pa] = pb; ans --; } } vector<int> cnt(N); for (int i = k; i >= 1; i --) { // 倒着做 cnt[i] = ans; set<string> s; for (auto v : g[op[i]]) { if (!vis.count(v)) { s.insert(find(v)); string pa = find(op[i]), pb = find(v); if (pa != pb) { p[pa] = pb; } } } ans -= (int)(s.size()) - 1; vis.erase(op[i]); } cnt[0] = ans; for (int i = 0; i <= k; i ++) { cout << cnt[i] << "\n"; } return 0; }