CF1997(edu168)题解 A-F
A. Strong Password
注意到最大效果是在两个相同字符之间插入一个不同的,贡献为 3。
否则在一开始插入一个和首位不同的,贡献为 2。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve()
{
string s; cin >> s;
bool ok = 0;
for(int i = 0; i < s.size() - 1; i ++)
{
if(s[i] == s[i + 1])
{
ok = 1;
cout << s.substr(0, i + 1) + (char)((s[i] - 'a' + 1) % 26 + 'a') + s.substr(i + 1) << "\n";
break;
}
}
if(!ok) cout << (char)((s[0] - 'a' + 1) % 26 + 'a') << s << "\n";
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
int t;cin >> t;while(t --) solve();
return 0;
}
B. Make Three Regions
注意到如果要分割成三部分,那么这一块的三面一定都要是 .
。
设新堵上的块为 o
,图应该长这样:
.o.
?.?
因为三块不连通,所以 ?
应该是 x
。
所以
图应该长这样(两种):
.o. | x.x
x.x | .o.
因为保证原图联通,所以直接判断有几个像这样的图形就行了。
C. Even Positions
直接考虑贪心填右括号,不用担心右括号填多了出现 (())))((
的情况。
因为最差也可在每个右括号前补一个左括号(奇数位都可以自己选择)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;
void solve()
{
int n; string s; cin >> n >> s;
int ans = 0;
stack<int> stk;
for(int i = 0; i < s.size(); i ++)
if(stk.size() && s[i] != '(') ans += i - stk.top(), stk.pop();
else stk.push(i);
cout << ans << "\n";
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
int t;cin >> t;while(t --) solve();
return 0;
}
上面用栈维护左括号位置,还可以拆贡献,只要维护未配对左括号个数即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;
void solve()
{
int n; string s; cin >> n >> s;
int ans = 0;
int c = 0;
for(int i = 0; i < s.size(); i ++)
{
if(c && s[i] != '(') c --;
else c ++;
ans += c;
}
cout << ans << "\n";
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
int t;cin >> t;while(t --) solve();
return 0;
}
D. Maximize the Root
因为节点 \(u\) 做操作不会影响父节点,所以可以 dp,设 \(f(u)\) 表示 \(u\) 子树内最小值最大是多少。
设 \(g(u)=\min_{v\in sons(u)}f(v)\)
有 \(f(u)=\min(a_u,g(u))\)。
注意到当 \(a_u<g(u)\) 时, \(a_u\) 成为 \(f(u)\) 的瓶颈,于是可以通过操作增大 \(a_u\),减小 \(g(u)\) 做到平衡。
所以可以让 \(a_u\) 和 \(g_u\) 都变成 \(\dfrac{g(u)+a_u}{2}\) 即可。
注意下取整一下。
注意到如果 \(a_u>g(u)\),那么答案不会变大,\(f(u)=g(u)\),特判一下。
所以有
\(f(u)=\min\left(a_u,\dfrac{\min_{v\in sons(u)}f(v)+a_u}{2}\right)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;
ll n, f[N], a[N];
vector<int> e[N];
void dfs(int x)
{
if(e[x].empty()) return f[x] = a[x], void();
f[x] = 0;
ll mn = 2e9;
for(int i : e[x])
{
dfs(i);
mn = min(mn, f[i]);
}
if(x == 1) cout << a[1] + mn << "\n";
if(a[x] >= mn) f[x] = mn;
else f[x] = (mn + a[x]) / 2;
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i ++) e[i].clear();
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 2; i <= n; i ++)
{
int x; cin >> x;
e[x].push_back(i);
}
dfs(1);
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
int t;cin >> t;while(t --) solve();
return 0;
}
二分答案也不是不行。但是都会二分答案了还不会线性做法吗?
E. Level Up
法一:主席树
注意到 \(\sum_{i=1}^n \frac 1i=O(n\log n)\)。
所以离线下来,对于每个 \(k\) 枚举等级增加的位置,也就是 \(O(n\log n)\) 个。
于是问题变成:求最小的 \(r\) 使得 \(\sum_{i=l}^r[a_i\geq lev]=k\),这可以在 \(a_i\) 的大小这一维上可持久化一下,以 \(i\) 为下标,线段树上二分一下就可以了,复杂度 \(O(n\log^2n)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5, V = 2e5;
int rt[N];
int fdp;
struct sgt
{
int a[N << 6], ls[N << 6], rs[N << 6], idx;
inline void pu(int x) {a[x] = a[ls[x]] + a[rs[x]];}
int upd(int q, int l, int r, int x, int v)
{
int u = ++idx; a[u] = a[x], ls[u] = ls[x], rs[u] = rs[x];
if(l == r) return a[u] += v, u;
int mid = l + r >> 1;
if(mid >= q) ls[u] = upd(q, l, mid, ls[x], v);
else rs[u] = upd(q, mid + 1, r, rs[x], v);
pu(u); return u;
}
void fnd(int l, int r, int x, int k)
{
if(l == r) return fdp = l, void();
int mid = l + r >> 1;
if(a[ls[x]] >= k) return fnd(l, mid, ls[x], k);
else return fnd(mid + 1, r, rs[x], k - a[ls[x]]);
}
int qry(int ql, int qr, int l, int r, int x, int k)
{
if(ql > qr) return 0;
if(ql <= l && r <= qr)
{
if(a[x] < k) return a[x];
return fnd(l, r, x, k), k;
}
int mid = l + r >> 1, ans = 0;
if(mid >= ql) ans += qry(ql, qr, l, mid, ls[x], k);
if(ans < k && mid < qr) ans += qry(ql, qr, mid + 1, r, rs[x], k - ans);
return ans;
}
}t;
vector<pair<int, int>> q[N];
int n, ans[N], Q, a[N];
vector<int> v[N];
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i ++) cin >> a[i], v[a[i]].push_back(i);
for(int i = V; i >= 1; i --)
{
rt[i] = rt[i + 1];
for(int j : v[i])
rt[i] = t.upd(j, 1, n, rt[i], 1);
}
for(int i = 1; i <= Q; i ++)
{
int x, y; cin >> x >> y;
q[y].push_back({x, i});
}
for(int i = 1; i <= n; i ++)
{
if(q[i].empty()) continue;
vector<int> pos;
int lst = 0;
for(int j = 1; j <= n / i; j ++)
{
if(t.qry(lst + 1, n, 1, n, rt[j], i) < i) break;
pos.push_back(fdp);
lst = fdp;
}
for(auto [j, id] : q[i])
{
int lev = lower_bound(pos.begin(), pos.end(), j) - pos.begin() + 1;
ans[id] = a[j] >= lev;
}
}
for(int i = 1; i <= Q; i ++)
cout << (ans[i] ? "YES" : "NO") << "\n";
return 0;
}
法二:树状数组
考虑上面的无脑做法,发现可以通过把 \(a_i\) 的大小这一维离线考虑,这样就不要可持久化了。
从小到大考虑 \(i\),找到从上一个位置开始的第 \(k\) 个 \(\geq i\) 的 \(a_j\),这也不用线段树,只要用树状数组上二分找到第 \(k+\left(上一个位置前有几个\geq i 的数\right)\) 个数即可。
每次考虑完一个 \(i\),就把 \(a_j=i\) 的 \(j\) 删除。
常数很小。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5, V = 2e5;
struct bit
{
static const int K = 18;
int a[(1 << K) + 5];
void upd(int x, int v)
{
for(int i = x; i <= (1 << K); i += (i & -i)) a[i] += v;
}
int qry(int x)
{
int ans = 0;
for(int i = x; i; i -= (i & -i)) ans += a[i];
return ans;
}
int lower_bound(int k)
{
int now = 1 << K;
if(a[now] < k) return 1e9;
for(int i = K - 1; i >= 0; i --)
if(a[now - (1 << i)] >= k) now = now - (1 << i);
else k -= a[now - (1 << i)];
return now;
}
} t2;
int n, Q, a[N];
vector<int> v[N];
vector<int> pos[N];
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i ++) cin >> a[i], v[a[i]].push_back(i);
for(int i = 1; i <= n; i ++) t2.upd(i, 1), pos[i].push_back(0);
for(int i = 1, r = n; i <= n; i ++)
{
for(int j = 1; j <= r; j ++)
{
int p = t2.lower_bound(t2.qry(pos[j].back()) + j);
if(p <= n) pos[j].push_back(p);
else r = j - 1;
}
for(int j : v[i]) t2.upd(j, -1);
}
for(int i = 1; i <= Q; i ++)
{
int x, y; cin >> x >> y;
int lev = lower_bound(pos[y].begin(), pos[y].end(), x) - pos[y].begin();
cout << ((a[x] >= lev) ? "YES\n" : "NO\n");
}
return 0;
}
法三:更简单的树状数组
更简单的:
和其他的方法解题角度不一样,其他方法是考虑对于每个 \(k\),第 \(i\) 个位置等级是多少,这个想法是考虑第 \(i\) 个位置,在 \(k\ge t_i\) 的时候答案从 NO
变成 YES
。
代码简单跑得快。
考虑二分 \(t_i\),注意到求在 \(i\) 前面有多少次交手等于求出 \(\sum_{j=1}^{i-1}[t_j\le mid]\)。因为从前往后求,后面没贡献,所以等于求 \(\sum_{j=1}[t_j\le mid]\)。
树状数组维护 \(t\) 即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5, V = 2e5;
struct bit
{
static const int K = 18;
int a[(1 << K) + 5];
void upd(int x, int v)
{
for(int i = x; i <= (1 << K); i += (i & -i)) a[i] += v;
}
int qry(int x)
{
int ans = 0;
for(int i = x; i; i -= (i & -i)) ans += a[i];
return ans;
}
} t2;
int n, Q, a[N], t[N];
vector<int> v[N];
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 1; i <= n; i ++)
{
int l = 1, r = n;
while(l < r)
{
int mid = l + r >> 1;
if(t2.qry(mid) / mid + 1 <= a[i]) r = mid;
else l = mid + 1;
}
t2.upd(t[i] = r, 1);
}
for(int i = 1; i <= Q; i ++)
{
int x, y; cin >> x >> y;
cout << ((t[x] <= y) ? "YES\n" : "NO\n");
}
return 0;
}
法四:根号算法
令 \(B\) 为分治阈值。
对于 \(k\le B\),\(O(nB)\) 暴力模拟,处理出升级的位置即可。
对于 \(k> B\),升级的位置不超过 \(B\) 个,于是用 \(B\) 个指针维护升级的位置,从小到大枚举 \(k\),每个指针一定单调右移,指针移动总距离 \(O(n^2/B)\)。
维护两个指针 \(j-1,j\) 之间有几个数 \(\ge j\)。可以前缀和,但是更好的方法是直接维护 \(cnt_j\) 表示两个指针 \(j-1,j\) 之间有几个数 \(\ge j\)。
指针 \(j\) 右移,更新 \(cnt_j,cnt_{j+1}\)。
当 \(B=\sqrt n\),复杂度最小。
时间复杂度 \(O(n\sqrt n+q\log n)\),空间复杂度 \(O(n\log n)\)。
把询问离线可以做到时间复杂度 \(O(n\sqrt n+q)\),空间复杂度 \(O(n + q)\)。
但是因为常数原因,\(B\) 开大一点跑得更快。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5, V = 2e5, B = 1000;
vector<int> v[N];
int n, Q, a[N];
vector<int> pos[N];
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i ++) cin >> a[i], v[a[i]].push_back(i);
for(int i = 1; i <= n; i ++) pos[i].push_back(0);
for(int i = 1; i <= min(n, B); i ++)
{
for(int j = 1, c = 0, k = 1; j <= n; j ++)
{
c += a[j] >= k;
if(c == i)
{
k ++, c = 0;
pos[i].push_back(j);
}
}
}
int pt[B + 5] = {}, cnt[B + 5] = {};
for(int i = 1; i <= min(n, B); i ++) pt[i] = 0;
for(int i = B + 1; i <= n; i ++)
{
int r = B;
for(int j = 1; j <= r; j ++)
{
while(pt[j] <= n && cnt[j] < i) pt[j] ++, cnt[j] += a[pt[j]] >= j, cnt[j + 1] -= a[pt[j]] >= j + 1;
if(pt[j] == n + 1) r = j - 1;
}
pos[i].resize(r + 1);
for(int j = 1; j <= r; j ++) pos[i][j] = pt[j];
}
for(int i = 1; i <= Q; i ++)
{
int x, y; cin >> x >> y;
int lev = lower_bound(pos[y].begin(), pos[y].end(), x) - pos[y].begin();
cout << ((a[x] >= lev) ? "YES\n" : "NO\n");
}
return 0;
}
F. Chips on a Line
注意到这很像斐波那契数列,又注意到 \(i\to i-1,i-2\),我们发现,一个位于 \(i\) 上的棋子等价于 \(fib_i\) 个位于 \(1\) 或 \(2\) 的棋子。(不断使用这个操作,反过来,可以使用逆操作)。
设 \(i\) 上有 \(c_i\) 个棋子,设状态 \(S=\sum_{i=1}^x c_i\times fib_i\)。
这个性质让我们发现,不同状态之间是不能转化的,相同状态之间是可以随便转化的,于是可以处理出每个 \(S\) 的代价。考虑 dp。
设 \(f(i)\) 为 \(S=i\) 时的代价。
于是有 \(f(i)=\min_{j=1}f(i-fib_j)+1\)。
然后考虑计数有多少个状态为 \(S\) 的方案,考虑背包,设 \(g(i,j,k)\) 表示 dp 到 \(fib_i\),填了 \(j\) 个棋子,状态为 \(k\) 的方案数,有:
\(g(i,j,k) = \sum_{c=0}g(i-1,j-c,k-c\times fib_i)\)。
复杂度不太对。
注意到这是一个完全背包,于是改写成:
\(g(i,j,k)=g(i,j-1,k-fib_i)+g(i-1,j,k)\)。
空间开不下。
滚动数组优化一下就好了。
答案就是 \(ans=\sum_{i=0} [f(i)=m]g(x,n,i)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1005, M = N * 60, p = 998244353;
int a[40];
int f[M], n, x, m;
int g[N][M];
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin >> n >> x >> m;
a[1] = a[2] = 1;
for(int i = 3; i < 40; i ++) a[i] = a[i - 1] + a[i - 2];
memset(f, 0x3f, sizeof f);
f[0] = 0;
for(int i = 1; i < M; i ++)
for(int j = 0; j < 40; j ++)
if(i >= a[j]) f[i] = min(f[i], f[i - a[j]] + 1);
g[0][0] = 1;
for(int i = 1; i <= x; i ++)
{
for(int j = 1; j <= n; j ++)
for(int k = a[i]; k <= n * 55; k ++)
g[j][k] = (g[j][k] + g[j - 1][k - a[i]]) % p;
}
ll ans = 0;
for(int i = 1; i < M; i ++)
ans += (f[i] == m) * g[n][i];
cout << ans % p;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效