这场比赛当年是用牛客竞赛平台线上比赛的,比赛链接:https://ac.nowcoder.com/acm/contest/35146

三人VP(xht,mqy,我),评价为全部唐完了,尤其是前两个小时更是唐中唐。
L. Polygon
还以为是什么计算几何题目呢,兴冲冲打开,发现是签到,白高兴一场。
所有边可以组成一个多边形,当且仅当除去一条最长边后剩余边的长度和大于最长边。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
i64 n, x; cin >> n;
i64 sum = 0, m = 0;
while(n--) {
cin >> x;
m = max(m, x);
sum += x;
}
sum -= m;
if(m >= sum) cout << "NO\n";
else cout << "YES\n";
return 0;
}
E. Plus
诈骗题,一开始就怀疑只有很少的答案,推导了一下发现确实如此。
若 p , q p, q p,q 均为奇素数,则 p q + q p p^q + q^p pq+qp 是一个大于 2 2 2 的偶数,一定不是奇数。显然 p = 2 p = 2 p=2 时才可能有解。若 q = 2 q = 2 q=2, p q + q p = 4 p^q + q^p = 4 pq+qp=4,舍去。当 q = 3 q = 3 q=3 时,合法, p q + q p = 17 p^q + q^p = 17 pq+qp=17;其余情况, q q q 是一个奇质数,且 q q q 不是 3 3 3 的倍数, 2 q m o d 3 = 1 , q 2 m o d 3 = 2 2^q \mod 3 = 1, q^2 \mod 3 = 2 2qmod3=1,q2mod3=2, p q + q p p^q + q^p pq+qp 是 3 3 3 的倍数,舍去。答案最多只有一对。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
i64 n; cin >> n;
if(n <= 2) cout << "0\n";
else cout << "1\n2 3\n";
return 0;
}
D. Game
此时 I 题搞得我和 xht 很红温,mqy 大致想到了如何判断必胜方,xht 完善了思路然后写了个莫队。第一发交上去 TLE 了,然后发现排序的 cmp 函数里不是比较两个区间的 l B \frac{l}{B} Bl,而是直接比较了 l l l,相当于分块分了 n n n 块,难怪会超时。改了一下就过了,过的第三题居然是这个,很神奇。
这题相当于我完全没参与,赛后自己想和写了一遍。手摸发现设 f ( x ) f(x) f(x) 表示数字 x x x 在区间 [ l , r ] [l,r] [l,r] 的出现次数。先手必胜当且仅当 ∃ x , f ( x ) ≡ 1 ( m o d 2 ) \exist x,f(x) \equiv 1 \pmod 2 ∃x,f(x)≡1(mod2)。详细证明题解中有叙述,直接截图放在底下。



#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
struct {
int cnt[N], oddcnt;
void insert(int x) {
if(cnt[x] & 1) oddcnt--;
else oddcnt++;
cnt[x]++;
}
void erase(int x) {
if(cnt[x] & 1) oddcnt--;
else oddcnt++;
cnt[x]--;
}
} mp;
int n, m, ans[N], a[N], B;
struct question {
int index, l, r;
bool operator < (const question &o) {
int b = l / B, bo = o.l / B;
if(b == bo) {
if(b & 1) return r > o.r;
else return r < o.r;
} else return b < bo;
}
};
vector<question> q;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
B = floor(sqrt(n));
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
for(int i = 1; i <= m; i++) {
int l, r; cin >> l >> r;
q.push_back({i, l, r});
}
sort(all(q));
int nl = 1, nr = 1;
mp.insert(a[1]);
for(auto [i, l, r] : q) {
while(nr < r) mp.insert(a[++nr]);
while(nl > l) mp.insert(a[--nl]);
while(nr > r) mp.erase(a[nr--]);
while(nl < l) mp.erase(a[nl++]);
if(mp.oddcnt) ans[i] = 1;
}
for(int i = 1; i <= m; i++) {
if(ans[i]) cout << "Alice\n";
else cout << "Bob\n";
}
return 0;
}
I. Generator
这题是我们全场表现最唐的一道题目,干的事情包括但不限于:
- 用 Python 打表尝试算出前 x x x 项的调和级数和,但是用的是分数类,根本跑不动
const int eu = 0.57721566540153L;,不知道这个int怎么写出来的- 以为用 C++ 的 long double 计算出的数的精度不够,所以想用 Python 打表,后来发现实际上精度非常够用
手推公式 + 打表发现输入对于 n n n ,答案是 1 + ∑ k = 1 n − 1 1 k 1 + \sum_{k = 1}^{n - 1} \frac{1}{k} 1+∑k=1n−1k1 。显然直接暴力计算实在是太慢了。有一个小知识: lim n → ∞ ∑ k = 1 n 1 k − ln n = γ \lim_{n \rightarrow \infty} \sum_{k = 1}^{n} \frac{1}{k} - \ln n = \gamma limn→∞∑k=1nk1−lnn=γ,这里 γ \gamma γ 是欧拉常数。
用 long double 计算出前 1 0 9 10^9 109 项调和级数的和,再减去 ln 1 0 9 \ln 10^9 ln109 就得到了一个可用的欧拉常数的估计值。虽然有浮点误差和截断误差,但也应该大致够用。当 m m m 不够大的时候, ln m + γ \ln m + \gamma lnm+γ 作为前 m m m 项的调和级数的和的估计值精确度是欠缺的,因此直接暴力计算即可。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
const auto eu = 0.57721566540153L;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
i64 n; cin >> n;
long double ans = 0;
if(n <= 100'000'000) {
for(int i = 1; i < n; i++) {
ans += 1.0L / i;
}
} else {
ans = logl(n - 1) + eu;
}
cout << fixed << setprecision(10) << ans + 1 << endl;
return 0;
}
K. Maze
直接搜就行,单个测试点复杂度为
O
(
4
n
2
m
)
O(4n^2m)
O(4n2m),没什么好说的。注意代码写法不要太繁琐。里面那一句 memset 实际上不是正确复杂度,但是考虑到
t
t
t 较小且 memset 较快,为了少写一点就直接这么做了。如果
t
t
t 较大就只能初始化范围内的。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 102;
char mp[N][N];
int n, m, vis[N][N][N][4];
const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};
struct state {int x, y, c, d;};
inline int &v(state &A) {
return vis[A.x][A.y][A.c][A.d];
}
void solve() {
cin >> n >> m;
for(int i = 1; i <= n; i++) {
mp[0][i] = mp[n + 1][i] = '*';
mp[i][0] = mp[i][n + 1] = '*';
}
for(int i = 1; i <= n; i++) {
string s; cin >> s;
for(int j = 1; j <= n; j++)
mp[i][j] = s[j - 1];
}
memset(vis, -1, sizeof(vis));
queue<state> q;
for(int i = 0; i < 4; i++) {
state t = {1, 1, 0, i};
q.push(t); v(t) = 0;
}
while(!q.empty()) {
for(int i = 0; i < 4; i++) {
auto p = q.front();
p.c = (i == p.d ? p.c + 1 : 1);
p.x += dx[i], p.y += dy[i], p.d = i;
if(p.c <= m && v(p) == -1 && mp[p.x][p.y] != '*') {
v(p) = v(q.front()) + 1;
q.push(p);
if(p.x == n && p.y == n) {
cout << v(p) << endl;
return;
}
}
}
q.pop();
}
cout << -1 << endl;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
G. Hot Water Pipe
可以一眼发现,对于同样温度的一整个连续段,应当绑在一起维护。可以发现,推进管子的段的数量是 O ( n ) O(n) O(n) 的,因此使用的段也是 O ( n ) O(n) O(n) 的,直接暴力模拟即可。
一开始读错题目了,以为是降低到 T min T_{\min} Tmin 时瞬间变成 T max T_{\max} Tmax,后来发现是降低到 T min − 1 T_{\min} - 1 Tmin−1 时瞬间加热。调试发现这个问题后直接在读入 T min T_{\min} Tmin 后将其减一就解决了。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
i64 n, m, tmin, tmax;
i64 nowtime;
struct block {
i64 length;
i64 pushtime;
i64 temperature;
block(i64 l, i64 t, i64 tp) {
length = l;
pushtime = t;
temperature = tp;
}
i64 getnowtemp() {
i64 ret = temperature;
i64 pt = nowtime - pushtime;
i64 mod = tmax - tmin;
ret = ret - tmin;
ret = ret - pt;
ret = (ret % mod + mod) % mod;
if(ret == 0) ret = mod;
ret += tmin;
return ret;
}
};
deque<block> hotpipe;
i64 pipelength = 0;
i64 solve(i64 len) {
i64 ans = 0;
while(!hotpipe.empty() && len > 0) {
auto b = hotpipe.back();
hotpipe.pop_back();
pipelength -= b.length;
if(b.length <= len) {
ans += b.length * b.getnowtemp();
len -= b.length;
} else {
ans += len * b.getnowtemp();
b.length -= len;
hotpipe.push_back(b);
pipelength += b.length;
len = 0;
}
}
if(len > 0) {
ans += tmax * len;
len = 0;
}
len = n - pipelength;
if(len > 0) {
hotpipe.push_front(block(len, nowtime, tmax));
pipelength = n;
}
return ans;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> tmin >> tmax;
tmin--;
pipelength = n;
for(int i = 1; i <= n; i++) {
int a; cin >> a;
hotpipe.push_back(block(1, 0, a));
}
while(m--) {
i64 t, k; cin >> t >> k;
nowtime += t;
cout << solve(k) << endl;
}
return 0;
}
B. Capital Program
本题可参考原题洛谷 P5536,做法很多样。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
int n, k, dep[N], deepest, fa[N], maxdep[N];
vector<int> edge[N];
void dfs(int u, int f) {
dep[u] = dep[f] + 1;
fa[u] = f;
maxdep[u] = dep[u];
if(dep[u] > dep[deepest]) {
deepest = u;
}
for(int x : edge[u]) {
if(x == f) continue;
dfs(x, u);
maxdep[u] = max(maxdep[u], maxdep[x]);
}
}
int check(int ans) {
int ret = 0;
for(int i = 1; i <= n; i++) {
if(maxdep[i] - dep[i] >= ans) {
ret++;
}
}
return ret;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
dep[0] = -1;
deepest = 0; dfs(1, 0);
int dpt = deepest;
deepest = 0; dfs(dpt, 0);
int len = dep[deepest];
for(int i = 1; i <= len / 2; i++) {
deepest = fa[deepest];
}
dpt = deepest;
deepest = 0; dfs(dpt, 0);
int L = 0, R = (len + 1) / 2, mid;
while(L < R) {
mid = (L + R) / 2;
if(check(mid) <= k) {
R = mid;
} else L = mid + 1;
}
cout << L << endl;
return 0;
}
C. Segment Tree
我读了伪代码发现是一个动态开点线段树,把题意给了队友,顺便给出了我的观察:如果线段树的区间 [ 1 , m ] [1, m] [1,m] 中的 m = 2 x m = 2^x m=2x,可以有一个贪心做法,每一层的建出的点的数量就是 min ( q , 2 i ) \min(q, 2^i) min(q,2i)。
一般的线段树的子节点会在 k k k 和 k + 1 k + 1 k+1 层。去掉这两层,前面的部分还是一样。最后的这两层有一些单独的点,也有一些“人”字形的三个点的树。三点树的根显然应该尽可能多取。最后再加上 q q q 个叶子节点即可。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
void solve() {
i64 m, q; cin >> m >> q;
if(q >= m) q = m;
i64 x = 0, ans = 0;
while((1LL << (x + 1)) <= m) x++;
for(int i = 0; i < x; i++)
ans += min(1LL << i, q);
ans += min(m - (1LL << x), q);
cout << ans + q << endl;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
F. Tree Path
我和 xht 很快想到了树链剖分。我们意识到,我们可以在基于 DFS 序建立的线段树上,维护经过某个点 x x x 的所有路径的集合。虽然涉及到线段树的区间修改,但只有单点查询,所以不用标记下传,直接用类似标记永久化的手段即可。因此,被添加到线段树内的数据量总共是 O ( n log 2 n ) O(n \log^2 n) O(nlog2n) 的。然后我们就陷入了思考的停滞,因为要查询的是不经过 x x x 的路径,而上述维护的是经过 x x x 的路径。
赛后发现,树链剖分后,一根链 ( u , v ) (u, v) (u,v) 可以被拆分成 O ( log n ) O(\log n) O(logn) 段连续 DFS 序的区间,这也就意味着,这棵树剩余的所有部分,虽然可能看上去零碎,但本质上只是一个补集,因此剩余部分也是 O ( log n ) O(\log n) O(logn) 个区间,上述结构维护的就是不经过 x x x 的路径了。
顺便一说,出题人的两个解法都与树链剖分无关,但是赛场上的四个 AC 似乎都是树链剖分,这是怎么回事呢。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
int dfsn[N], dfso[N], dfsc, fa[N];
int hson[N], top[N], dep[N], sz[N];
vector<int> edge[N], son[N];
int n, k, m, nowmin;
const int inf = 1e9;
void dfs1(int u, int f) {
sz[u] = 1, fa[u] = f;
dep[u] = dep[f] + 1;
hson[u] = 0;
for(int x : edge[u]) {
if(x != f) {
dfs1(x, u);
sz[u] += sz[x];
if(sz[x] > sz[hson[u]]) {
hson[u] = x;
}
}
}
for(int x : edge[u]) {
if(x != f && x != hson[u]) {
son[u].push_back(x);
}
}
}
void dfs2(int u, int f) {
dfsn[u] = ++dfsc;
dfso[dfsc] = u;
if(u == hson[f]) {
top[u] = top[f];
} else top[u] = u;
if(hson[u]) dfs2(hson[u], u);
for(int x : son[u]) {
dfs2(x, u);
}
}
void split(int u, int v, vector<pii> &vec) {
vec.clear();
while(top[u] != top[v]) {
if(dep[top[u]] > dep[top[v]]) swap(u, v);
vec.push_back({dfsn[top[v]], dfsn[v]});
v = fa[top[v]];
}
if(dep[u] > dep[v]) swap(u, v);
vec.push_back({dfsn[u], dfsn[v]});
sort(all(vec));
}
vector<int> st[N << 2];
void update(int l, int r, int x, int p = 1, int L = 1, int R = n) {
if(l <= L && R <= r) {
st[p].push_back(x);
return;
} else {
int mid = (L + R) / 2;
if(l <= mid) update(l, r, x, p << 1, L, mid);
if(r > mid) update(l, r, x, p << 1 | 1, mid + 1, R);
}
}
int query(int x, int p = 1, int L = 1, int R = n) {
while(!st[p].empty() && st[p].back() < nowmin) st[p].pop_back();
int ret = st[p].empty() ? inf : st[p].back();
if(L == R) return ret;
int mid = (L + R) / 2;
if(x <= mid) return min(ret, query(x, p << 1, L, mid));
else return min(ret, query(x, p << 1 | 1, mid + 1, R));
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k >> m;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
dfs1(1, 0);
dfs2(1, 0);
vector<pair<int, pii>> paths;
for(int i = 1; i <= k; i++) {
int p, q, v;
cin >> p >> q >> v;
paths.push_back({v, {p, q}});
}
sort(all(paths));
reverse(all(paths));
for(auto [val, pa] : paths) {
auto [u, v] = pa;
vector<pii> vec;
split(u, v, vec);
vector<pii> rvec;
if(vec[0].first > 1) {
update(1, vec[0].first - 1, val);
}
for(int i = 1; i < vec.size(); i++) {
int L = vec[i - 1].second + 1;
int R = vec[i].first - 1;
if(L <= R) update(L, R, val);
}
if(vec.back().second < n) {
update(vec.back().second + 1, n, val);
}
}
int last = 0, op, x;
while(m--) {
cin >> op;
if(op == 0) {
paths.pop_back();
nowmin = paths.empty() ? inf : paths.back().first;
} else {
cin >> x; x ^= last;
last = query(dfsn[x]);
if(last == inf) last = -1;
cout << last << endl;
}
}
return 0;
}
J. Papers
非常好题目,我连基础性质都没发现,直接死在了第一步。
官方题解:一定存在一种最优方案,使得其中没有任何一件 paper 是在做其他的 paper 中间开始的(中间的含义里不包括开始和结束)。可以假设存在这样的 paper,找到最后一个在其他 paper 中间开始的,把它开始时所有的 paper 向后平移,不影响总时间,甚至可能变得更优。
于是这个问题就被转化为了一个背包问题:已知有 m m m 个物品,第 i i i 个物品占据空间为 i i i ,开销为 a i a_i ai ,每次询问给出一个 n n n ,求装满大小正好为 n n n 的背包的最小开销。不过这里有一个问题: n n n 太大了,没办法直接做。不过由于物品的占据空间最大为 m m m ,所以当 n > m 2 n > m^2 n>m2 时可以贪心地选取性价比最高的那个,只需要背包做出 m 2 m^2 m2 内的答案即可。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
i64 dp[160410], a[410], n, m, q;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> m;
for(int i = 1; i <= m; i++) {
cin >> a[i];
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for(int i = 0; i < m * m; i++) {
for(int j = 1; j <= m; j++) {
dp[i + j] = min(dp[i] + a[j], dp[i + j]);
}
}
int mi = 1;
for(int i = 1; i <= m; i++) {
if(a[i] / (double)i < a[mi] / (double)mi) mi = i;
}
cin >> q;
while(q--) {
i64 n; cin >> n;
i64 ans = 0;
if(n > m * m) {
i64 t = ((n - (m * m + 1)) / mi) + 1;
ans += t * a[mi];
n -= t * mi;
}
ans += dp[n];
cout << ans << endl;
}
return 0;
}
AHM 官方题解
这场比赛的题解不太容易找到,我把剩下三题 (AHM) 的题解截图附在底下,不过我自己不打算写了。






1245

被折叠的 条评论
为什么被折叠?



