共111支队伍,获奖情况(大概)
铜牌66 —— 3 296
银牌33 —— 4 391
金牌 11 —— 6 808
题目难度(过题)L F E B C I J D K H
Problem - L - Codeforces
思路:注意到答案是连乘,只要有0那全部为0,而 > 10的区间一定有0,10之内的模拟即可
// Code Start Here int t;cin >> t;while(t--){int l , r;cin >> l >> r;if(r - l >= 10)cout << 0 << endl;else{int ans = 1;for(int i = l;i<=r;i++){int now = i;while(now){ans *= now % 10;ans %= md;now /= 10;}}cout << ans << endl;}}
Problem - F - Codeforces
思路:我们肯定想到最简单的方法就是直接恰好一轮mod,然后判断一下 x + y - z的值
提交,wa2
仔细来想一下,这个题有坑。
当k < x 或 z时 ,根本没法到x , z这两个时间,只会变成x % k和 z % k
分类讨论一下,看 k 和x , z的关系
提交 通过
// code start hereint t;cin >> t;while(t--){int x , y , z;cin >> x >> y >> z;if(x + y == z)cout << z + 1 << endl;else if( x <= z && x + y > z){if(x + y - z > z)cout << x + y - z <<endl;else cout << -1 << endl;}else if(x > z && y > z)cout << x + y - z <<endl;else cout << -1 << endl;}
Problem - E - Codeforces
观察到这个E要求求的是一个最大值的最小值,考虑二分的可行性。
我们注意到一选L一定是一个连续的区间,无论有多少个1,后面有多少个0,后面都要直接全选。
二分长度后贪心
// Code Start Here int t;cin >> t;while(t--){int n , k;cin >> n >> k;string s;cin >> s;auto check = [&](int x)->bool{int now = 0;int i = n - 1;while(i >= 0){if(s[i] == '1')now ++ ,i -= x;else i--;}return k >= now;};if(s == string(n , '0'))cout << 0 << endl;else{int l = 1 , r = n;while(l < r){int mid = l + r >> 1;if(check(mid)) r = mid;else l = mid + 1;}cout << l << endl;}}return 0;
Problem - B - Codeforces
题意大概是一笔画问题,考虑往欧拉路径上靠。
根据欧拉路径的定义:一个图存在欧拉路径的充要条件是,图忽略方向后联通。最多只能有一个起点和终点,其他节点入度= 出度。
我们把每个格子变成一个节点,然后根据箭头建一条从当前格子指向另外一个格子的边。比如如果 (i,j)
是 'u'
且 a[i][j] = 2
,那么有一条边:(i,j) → (i-2,j)
建图后检查一下是否存在欧拉路径,然后判断一次联通即可
// Code Start Herevi dx(256), dy(256);dx['u'] = -1;dy['u'] = 0;dx['d'] = 1;dy['d'] = 0;dx['l'] = 0;dy['l'] = -1;dx['r'] = 0;dy['r'] = 1;int t;cin >> t;while (t--) {int n, m;cin >> n >> m;vector<str> a(n);vvi step(n, vi(m));for (int i = 0 ; i < n; i++)cin >> a[i];for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> step[i][j];}}vi indeg(n * m, 0), outdeg(n * m, 0);vvi g(n * m);vb used(n * m, false);for (int i = 0; i < n; ++i) {for (int j = 0; j < m; ++j) {// 看下一个在哪里int ni = i + dx[a[i][j]] * step[i][j];int nj = j + dy[a[i][j]] * step[i][j];int u = i * m + j;used[u] = true;if (ni >= 0 && ni < n && nj >= 0 && nj < m) {int v = ni * m + nj;indeg[v]++;outdeg[u]++;g[u].push_back(v);g[v].push_back(u);used[v] = true;}}}bool f = true;int st = 0, ed = 0;for (int i = 0; i < n * m; ++i) {if (used[i]) {if (outdeg[i] - indeg[i] == 1) st++;else if (indeg[i] - outdeg[i] == 1) ed++;else if (indeg[i] != outdeg[i]) {f = false;break;}}}if (!((st == 0 && ed == 0) || (st == 1 && ed == 1))) f = false;vb vis(n * m, false);auto dfs = [&](auto dfs, int u)->void{vis[u] = true;for (int v : g[u]) {if (!vis[v]) {dfs(dfs, v);}}};int l = -1;for (int i = 0; i < n * m; ++i) {if (used[i]) {l = i;break;}}dfs(dfs, l);for (int i = 0; i < n * m; ++i)if (used[i] && !vis[i]) {f = false;}if (f)cout << "Yes\n";else cout << "No\n";}return 0;
Problem - C - Codeforces
我们假设每一个子串都可以用来旋转变成一个新字符串,所有子串都会带来一个不同的结果,然后我们开始看不产生新串的重复情况:
纯 '0' 子串不会变;纯 '8' 子串不会变;‘6’ 与 ‘9’ 混搭会产生重复(如 “69” ↔ “96”)。
提交,wa。
思考,注意到并不是 所有 '6'
和 '9'
混合子串都变成相同的东西。比如 '669'
→ '996'
≠ '669'。
所以并不总是意味着重复,只是可能导致重复
// Code Start Here int t;cin >> t;while(t--){string s;cin >> s;int a = 0 , b = 0 , c = 0 , d = 0;for(auto i :s){if(i == '0')a++;else if(i == '6')b++;else if(i == '8')c++;else d++;}int ans = 0;ans += (1 + sz(s)) * sz(s) / 2;ans -= (1 + a) * a / 2;ans -= (1 + c) * c / 2;ans -= 1 * b * d;if(b != sz(s) && d != sz(s))ans++;cout << ans << endl;}
Problem - I - Codeforces
题意:给定一棵无根树,每条边带有一个小写字母,问:有多少个节点可以作为根,使得这棵树变成一个合法的字典树
思路:一个 trie 合法需要满足
1、每个节点表示一条从根到该节点的字符串路径
2、所有节点代表的字符串必须是互不相同
3、字符串是沿着边上的字符拼接得到的
换句话说:从根出发,不能走出两条完全相同的路径。
我们可以逐个节点检查它连出的边,主要分为以下几种情况:
首先是不合法的情况:
1:某个节点连出三条或更多相同字符的边 ,肯定构成冲突 , 无解
2:某个节点连出两种不同的字符,各重复两次,例如 a a b b,无解
其次观察合法情况:
当一个节点u 恰好有两条边使用了相同字符,例如 a a,如果这两条边通向的节点 x,y 都存在,那么,路径从根出发到 x 和 y在字符上就会重复。因此必须让根节点选在某个区域,避免让 x 和 y 出现在同一路径上
以下引出本题核心判断:
1:根节点位置的分类讨论
我们选定任意一个根进行 DFS(只是为了编号),然后设:
-
in[u]
: u 的 DFS 进入时间(序号) -
siz[u]
: u 的子树大小
情况 1:in[u] < in[x], in[y],
说明 u 是 x 和 y 的祖先,根只能选在 x 的子树 或 y 的子树中,其余地方都不能选 ,标记为非法区间
情况 2:in[x] < in[u] < in[y],
说明 x 是 u 的祖先,y 是 u 的子树,根只能选在 y 的子树中 或在 u 的子树外部
因此,我们需要将这些非法的区间进行标记,最终统计哪些节点未被标记 ,他们就是合法根
子树大小可以用dfn解决,打非法区间标记可以使用线段树
template<class Info, class Tag>
struct LazySegmentTree {int n;vector<Info> info;vector<Tag> tag;LazySegmentTree(const vector<Info>& init) {n = init.size() - 1;info.assign(4 * n, Info());tag.assign(4 * n, Tag());function<void(int, int, int)> build = [&](int p, int l, int r) {if (l == r) {info[p] = init[l];return;}int m = (l + r) / 2;build(p * 2, l, m);build(p * 2 + 1, m + 1, r);pull(p);};build(1, 1, n);}void pull(int p) {info[p] = info[2 * p] + info[2 * p + 1];}void apply(int p, const Tag &v) {info[p].apply(v);tag[p].apply(v);}void push(int p) {apply(2 * p, tag[p]);apply(2 * p + 1, tag[p]);tag[p] = Tag();}void rangeApply(int p, int l, int r, int x, int y, const Tag &v) {if (r < x || l > y) return;if (x <= l && r <= y) {apply(p, v);return;}push(p);int m = (l + r) / 2;rangeApply(p * 2, l, m, x, y, v);rangeApply(p * 2 + 1, m + 1, r, x, y, v);pull(p);}void rangeApply(int l, int r, const Tag &v) {if (l > r) return;rangeApply(1, 1, n, l, r, v);}Info rangeQuery(int p, int l, int r, int x, int y) {if (r < x || l > y) return Info();if (x <= l && r <= y) return info[p];push(p);int m = (l + r) / 2;return rangeQuery(p * 2, l, m, x, y) + rangeQuery(p * 2 + 1, m + 1, r, x, y);}Info rangeQuery(int l, int r) {return rangeQuery(1, 1, n, l, r);}
};struct Tag {ll x;Tag(ll x = 0) : x(x) {}void apply(const Tag &t) { x += t.x; }
};struct Info {ll x, l, r;Info(ll x = 0, ll l = 0, ll r = 0) : x(x), l(l), r(r) {}void apply(const Tag &t) {x += t.x * (r - l + 1);}
};Info operator+(const Info &a, const Info &b) {return Info(a.x + b.x, a.l, b.r);
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);int t;cin >> t;while (t--) {int n;cin >> n;vector<vector<pair<int, char>>> g(n + 1);for (int i = 1; i < n; ++i) {int u, v; char c;cin >> u >> v >> c;g[u].emplace_back(v, c);g[v].emplace_back(u, c);}bool f = false;for (int u = 1; u <= n; ++u) {map<char, int> cnt;for (auto [v, c] : g[u]) cnt[c]++;int tw = 0;for (auto [_, k] : cnt) {if (k >= 3) f = true;if (k == 2) ++tw;}if (tw >= 2) f = true;}if (f) {cout << 0 << endl;continue;}vector<int> dfn(n + 1), siz(n + 1);int timer = 0;function<void(int, int)> dfs = [&](int u, int p) {dfn[u] = ++timer;siz[u] = 1;for (auto &[v, c] : g[u]) {if (v != p) {dfs(v, u);siz[u] += siz[v];}}};dfs(1, 0);vector<Info> init(n + 1);for (int u = 1; u <= n; ++u) {init[dfn[u]] = Info(0, dfn[u], dfn[u]);}LazySegmentTree<Info, Tag> seg(init);for (int u = 1; u <= n; ++u) {unordered_map<char, int> mp;for (auto &[v, c] : g[u]) {if (!mp.count(c)) mp[c] = v;else {int f1 = mp[c], f2 = v;if (dfn[f1] > dfn[f2]) swap(f1, f2);if (dfn[f1] > dfn[u] && dfn[f2] > dfn[u]) {seg.rangeApply(1, dfn[f1] - 1, Tag(1));seg.rangeApply(dfn[f1] + siz[f1], dfn[f2] - 1, Tag(1));seg.rangeApply(dfn[f2] + siz[f2], n, Tag(1));}else if (dfn[f1] > dfn[u] && dfn[u] > dfn[f2]) {seg.rangeApply(dfn[u], dfn[f1] - 1, Tag(1));seg.rangeApply(dfn[f1] + siz[f1], dfn[u] + siz[u] - 1, Tag(1));}else if (dfn[f2] > dfn[u] && dfn[u] > dfn[f1]) {seg.rangeApply(dfn[u], dfn[f2] - 1, Tag(1));seg.rangeApply(dfn[f2] + siz[f2], dfn[u] + siz[u] - 1, Tag(1));}break;}}}int ans = 0;for (int u = 1; u <= n; ++u) {if (seg.rangeQuery(dfn[u], dfn[u]).x == 0) ++ans;}cout << ans << endl;}return 0;
}
Problem - J - Codeforces
题意:给定多个区间,从这些区间里面选数使得and和最大
思路:我们从高位往第0位尝试是否可以填1
记录每个区间目前构造的前缀 v,即当前已经选的高位部分,假设我们尝试在第 p 位填 1,那么我们希望每个区间都能选择一个数,其第 p 位为 1。也就是说,当前候选区间必须和有交集,如果所有区间都满足这个条件,那我们就将所有 v_i+= 2^p,把第 p 位设为 1。否则,说明这位只能是 0,此时我们判断:
- 哪些区间这一位只能填 0;
- 哪些区间这一位必须填 1(因为否则无法满足区间条件)
- 哪些区间这一位不影响答案,随意填
// Code Start Here int t;cin >> t;while(t--){int n;cin >> n;vpii ranges(n);for(auto &[l , r] : ranges)cin >> l >> r;vi v(n , 0);int ans = 0;for(int bit = 30 ; bit >=0 ;bit--){bool f = true;for(int i = 0;i<n;i++){int l = ranges[i].first;int r = ranges[i].second;int lo = v[i] + (1 << bit);int hi = v[i] + (1 << (bit + 1)) - 1;if(hi < l || lo > r){f = false;break;}}if(f){ans |= (1 << bit);for(int i = 0;i<n;i++)v[i] += (1 << bit);}else{for(int i = 0;i<n;i++){int l = ranges[i].first;int r = ranges[i].second;int lo = v[i] + (1 << bit);int hi = v[i] + (1 << (bit + 1)) - 1;if(hi < l || lo > r){continue;}int x = v[i] + (1 << bit) - 1;if(x < l)v[i] += (1 << bit);}}}cout << ans <<endl;}
Problem - D - Codeforces
题意:此题中我们定义两个点之间的最短距离是曼哈顿距离。
宝宝从A 走向商场C,速度为a。梦幻从B 开车,也要到C,速度为b>a。梦幻可以中途接上宝宝,两人一起前往C。找出宝宝到达C 的最短时间
思路:我们考虑两种策略:
1:宝宝独自走到商场
2:梦幻网格接上宝宝,一起前往商场
比较这两种方式谁用时更少。
假设你是 Dream,想要中途接宝宝。如果你在接他前绕了太远路,就白费了;那么最好的情况是找一个中转点 D接上。所以让 D 满足两个条件:
1:D 必须位于A 和B 组成的最小矩形R 中 ,不绕路
2:D 离C 尽可能近 , 接上之后离目标也近
共有三种情况:
-
宝宝自己走到 C
-
Dream先到 D,然后自己去商场(不接人)
-
Dream先到 D,然后追上宝宝一起去商场
其中后者用追击问题计算,模拟题。
// Code Start Here int t;cin >> t;while(t--){int a, b, xa, ya, xb, yb, xc, yc;cin >> a >> b >> xa >> ya >> xb >> yb >> xc >> yc;auto Manhattan = [&](int xa, int ya, int xb, int yb, int v)->double{return (abs(xb - xa) + abs(yb - ya)) * 1.0 / v;};double ans = Manhattan(xa, ya, xc, yc, a);int l = min(xa, xb), r = max(xa, xb);int d = min(ya, yb), u = max(ya, yb);int xd = max(l, min(r, xc));int yd = max(d, min(u, yc));double ta = Manhattan(xa, ya, xd, yd, a);double tb = Manhattan(xb, yb, xd, yd, b);if (ta < tb) {ans = min(ans, Manhattan(xb, yb, xc, yc, b));} else {double t = Manhattan(xa, ya, xb, yb, a + b);double dis = abs(xc - xa) + abs(yc - ya) - a * t;ans = min(ans, t + dis / b);}cout << point(6) << ans << '\n';}
Problem - K - Codeforces
题意:
有一个无向图,每个点有若干只怪物。每个点最多能被怪物随机封锁 d[i] 条出边。给定若干出口点,问从点 1 出发,在最坏情况下是否能逃离,并输出最短路径长度。
思路:
最坏情况意味着:从每个点出发时,最短的 d[i] 条边都无法使用。我们可以使用一种反向 Dijkstra + 堵塞次数判定的策略
我们不从点 1 出发,而从所有出口点出发,向其他点做 Dijkstra。
这样做的好处是:从出口到其他点的最短距离 = 从其他点到出口在图中路径反向后的最短距离。即最终得到的 dis[x] 实际上表示的是最坏情况下从点 x 到任意一个出口的最短路径长度。
核心问题是如何用 d[x] 模拟最坏情况的怪物堵路。每个点 x 有 d[x] 条边可能会被堵住。为了模拟最坏情况下,我们允许每个点被访问多次。前 d[x] 次访问全部忽略,相当于被怪物堵)。第 d[x] + 1 次访问才确定该点的最终 dis 值,认为这条路径不是被封锁的,因此可以走。即:
1:每次访问 u,都减少一个封路的怪物。
2:如果还有怪物封路,这次路径作废。
3:否则,说明已经没有怪物,我们可以更新 dis[u]
// Code Start Hereint t;cin >> t;while (t--) {int n, m, k;cin >> n >> m >> k;vector<int> d(n + 2, 0), dis(n + 2, -1);vector<int> exits(k);vector<vpii> g(n + 1, vector<pair<int,int>>());for (int i = 0; i < k; ++i) cin >> exits[i];for (int i = 1; i <= n; i++)cin >> d[i];for (auto i : exits) d[i] = 0;for(int i = 1;i<=m;i++){int u , v , w;cin >> u >> v >>w;g[u].emplace_back(v , w);g[v].emplace_back(u , w);}priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;for(auto i : exits)pq.emplace(0 , i);while (!pq.empty()) {auto [dd, u] = pq.top(); pq.pop();if (dis[u] != -1) continue;if (--d[u] >= 0) continue;dis[u] = dd;for (auto [v, w] : g[u]) {if (dis[v] == -1) {pq.emplace(dis[u] + w, v);}}}cout << dis[1] <<endl;}
Problem - H - Codeforces
题意:有 n 个学生编号为 1 到 n,每个学生最多参加一个配对。要求构造一个数组:
配对 (a, b) 满足:gcd(a, b) > 1。每个学生最多参与一次;尽可能多地构造配对;输出这个构造的数组。
思路:
我们优先保证大质数能使用其倍数构造;如果从小到大,可能被更小的质数先配走 i*2,造成损失;所以大质数优先。
其次收集所有 p 的倍数中未使用的数进行配对,处理奇数个的情况:加入 i * 2。i * 2 是 i 的倍数;在从大到小的顺序下,i * 2 此时尚未被使用(否则跳过);加入后,保证 vec.size() 为偶数。然后两两配对 , 标记使用。剩下的偶数做补充配对
// Code Start Here constexpr int N = 1e6+10;vector<bool> f(N,false);auto sieve = [&]()-> void {for (int i = 2; i * i < N; i++) {if (!f[i]) {for (int j = i * 2; j < N; j += i){f[j] = true;}}}};sieve();int t;cin >> t;while(t--){int n;cin >> n;vector<pair<int,int>> ans;vector<bool> vis(n + 10, false);if (n <= 3) {cout << 0 << endl;continue;}for (int i = n; i > 2; i--) {if (!f[i] && i * 2 <= n) {if(vis[i*2])continue;vector<int> vec = {i};for (int j = i * 3; j <= n; j += i) if (!vis[j]) vec.push_back(j);if (vec.size() % 2 == 1) vec.push_back(i * 2);for (size_t j = 1; j < vec.size(); j += 2) {vis[vec[j - 1]] = vis[vec[j]] = true;ans.emplace_back(vec[j - 1], vec[j]);}}}vector<int> res;for (int i = 2; i <= n; i += 2) if (!vis[i]) res.push_back(i);for (int i = 1; i < sz(res); i += 2) ans.emplace_back(res[i - 1], res[i]);cout << ans.size();for (auto [a, b] : ans) {cout << " " << a << " " << b;}cout << endl;}