"蔚来杯"2022牛客暑期多校训练营1 题解
A. Villages: Landlines
用坐标和半径转化为区间,问题转化为:
令\(n\)个区间的每一个区间至少与另外一个区间有交集,不相交则延长区间直到相交,问最小延长长度。
按右端点降序排序,每次遍历存储最小的左端点,发现当前区间右端点小于现在的最小左端点时更新答案。
#include<bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
const int MAXN = 2e5 + 5;
int n;
pii a[MAXN];
bool cmp(pii a, pii b) {
return a.fi + a.se > b.fi + b.se;
}
signed main()
{
ios::sync_with_stdio(false); cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].fi >> a[i].se;
sort(a + 1, a + 1 + n, cmp);
int ans = 0, minl = a[1].fi - a[1].se, lastl = minl, lastr = a[1].fi + a[1].se;
for(int i = 2; i <= n; i++) {
int cl = a[i].fi - a[i].se, cr = a[i].fi + a[i].se;
if(minl - cr > 0) ans += minl - cr;
minl = min(minl, cl);
lastl = cl, lastr = cr;
}
cout << ans << "\n";
return 0;
}
B. Spirit Circle Observation
符合要求的两个子串格式为\(Sp9999...999\)和\(S(p+1)0000...000\),其中\(p \in [0, 8]\)。
对原串构造SAM,枚举节点 \(i\) 找相同前缀,此时 \((st[i].len - st[ st[i].link ].len)\) 为 \(S\) 的不同长度,
接着枚举 \(p\),分别跳到 \(u=nxt[i][p]\) 和 \(v=nxt[i][p+1]\),此时 \(st[u].size\) 代表 \(u\) 的 \(endpos\) 集合,即 \(Sp\) 在原串中的不同位置,\(st[v].size\) 代表 \(v\) 的 \(endpos\) 集合,即 \(S(p+1)\) 在原串中的不同位置,
最后一直沿着9和0的出边走到头,用map存储此过程中经过的节点,直接计算贡献即可。
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define int long long
#define pir make_pair
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
const int MAXN = 4e5 + 5;
const int MOD = 1e9 + 7;
string S;
int n;
struct SAM {
int sz = 1, last = 1;
vector<int> G[MAXN << 1];
map<pii, int> Map;
struct state {
int len, link, size;
int nxt[10];
}st[MAXN << 1];
void Extend(int c) {
int cur = ++sz, p = last;
st[cur].size = 1;
st[cur].len = st[last].len + 1;
while(p && !st[p].nxt[c]) {
st[p].nxt[c] = cur;
p = st[p].link;
}
if(!p) st[cur].link = 1;
else {
int q = st[p].nxt[c];
if(st[p].len + 1 == st[q].len) st[cur].link = q;
else {
int clone = ++sz;
st[clone].len = st[p].len + 1;
for(int i = 0; i <= 9; i++) st[clone].nxt[i] = st[q].nxt[i];
st[clone].link = st[q].link;
while(p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
void DFS(int u) {
for(auto v : G[u]) DFS(v), st[u].size += st[v].size;
}
void MakeTree() {
for(int i = 0; i <= sz; i++) G[i].clear();
for(int i = 2; i <= sz; i++) G[ st[i].link ].push_back(i);
DFS(1);
}
int Calc(int u, int v) {
if(!u || !v) return 0;
if(Map[ {u, v} ]) return Map[ {u, v} ];
else return Map[ {u, v} ] = Calc(st[u].nxt[9], st[v].nxt[0]) + st[u].size * st[v].size;
}
void Calc() {
int ans = 0; st[0].size = 0; st[0].len = -1;
for(int i = 1; i <= sz; i++) {
for(int k = 0; k < 9; k++) {
int u = st[i].nxt[k], v = st[i].nxt[k + 1];
int cur = ans;
ans += (st[i].len - st[ st[i].link ].len) * Calc(u, v);
}
}
cout << ans << "\n";
}
}sam;
signed main()
{
cin >> n >> S;
for(auto i : S) sam.Extend(i - '0');
sam.MakeTree();
sam.Calc();
return 0;
}
C. Grab the Seat!
一个点能挡住的区域为\((0, 1)\)和\((0, m)\)到这个点的两条射线之间的区域。
分别考虑① \((0,1)\) 到 \(k\) 个点的射线的情况,② \((0, m)\) 到 \(k\) 个点的射线的情况。
考虑情况①,我们先对 \(\forall y \in [1,m]\) 找出 \(x\) 值最小的点,按 \(y\) 值从小到大加入射线,
在加入射线的过程中,我们发现斜率大的射线会覆盖斜率小的射线,我们在加入射线的时候维护一下斜率最大值,并以此更新 \(y\) 值固定时的答案上界。
情况②同理,最后直接枚举 \(y\) 值累加即可。
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define pir make_pair
#define fi first
#define se second
using namespace std;
const int MAXN = 2e5 + 5;
int n, m, k, q, f[MAXN], ans[MAXN];
pii a[MAXN], maxx, minn;
void Max(int x, int y) {
int cx = maxx.fi, cy = maxx.se;
if(y * cx > x * cy) maxx = pir(x, y);
}
void Min(int x, int y) {
int cx = minn.fi, cy = minn.se;
if(y * cx < x * cy) minn = pir(x, y);
}
signed main()
{
cin >> n >> m >> k >> q;
for(int i = 1; i <= k; i++) cin >> a[i].fi >> a[i].se;
while(q--) {
int p, cx, cy; cin >> p >> cx >> cy;
a[p] = pir(cx, cy);
for(int i = 1; i <= m; i++) f[i] = n + 1;
for(int i = 1; i <= k; i++) f[ a[i].se ] = min(a[i].fi, f[ a[i].se ]);
ans[1] = f[1], ans[m] = f[m];
maxx = pir(f[1], 0);
for(int i = 2; i <= m; i++) {
Max(f[i], i - 1);
ans[i] = min(f[i], (int)ceil(1.0 * maxx.fi * (i - 1) / maxx.se) );
}
minn = pir(f[m], 0);
for(int i = m - 1; i >= 1; i--) {
Min(f[i], i - m);
ans[i] = min(ans[i], (int)ceil(1.0 * minn.fi * (i - m) / minn.se) );
}
int tot = 0;
for(int i = 1; i <= m; i++) tot += ans[i] - 1;
cout << tot << "\n";
}
return 0;
}
F. Cut
只考虑第3个操作,我们可以用线段树维护 当前区间两个端点为奇数还是偶数 和 当前区间能满足题目要求的最长序列。
合并时对于最长序列首先直接加和,然后判断两端是否为01交替,是的话令答案自加1。
其答案最优性可以通过相同的0和1可以合并来考虑,区间的最长序列一定有一个方案会用到端点。
考虑前两个操作,我们将区间分为一个个连续段(排序后连续),并用一个标记来表示当前连续段为升序还是降序,
通过线段树合并和分裂即可维护这些连续段,
对于排序操作,我们进行线段树分裂,将端点部分的连续段分裂出来,再用线段树合并将询问区间内的所有连续段合并,具体看代码。
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define int long long
#define pir make_pair
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
const int MAXN = 1e5 + 5;
const int MOD = 1e9 + 7;
int n, m, Op[MAXN];
set<int> Set;
struct Node {
int ltype, rtype, ans, sze;
friend Node operator + (Node a, Node b) {
if(!a.sze) return b;
if(!b.sze) return a;
Node res;
res.ans = a.ans + b.ans + (a.rtype ^ b.ltype);
res.ltype = a.ltype;
res.rtype = b.rtype;
res.sze = a.sze + b.sze;
return res;
}
}tree[MAXN << 2];
struct Segment_Tree {
int root[MAXN * 40], son[MAXN * 40][3];
Node sum[MAXN * 40];
int pool[MAXN * 40], delcnt = 0, cnt = 0;
void PushUp(int p) { sum[p] = sum[ son[p][0] ] + sum[ son[p][1] ]; }
int NewNode() { return delcnt ? pool[delcnt--] : ++cnt; }
void DelNode(int p) {
pool[++delcnt] = p;
sum[p] = {0, 0, 0, 0};
son[p][0] = son[p][1] = 0;
}
void Insert(int& p, int l, int r, int loc) {
if(!p) p = NewNode();
if(l == r) {
sum[p].ltype = sum[p].rtype = (loc & 1);
sum[p].ans = 0, sum[p].sze = 1;
return ;
}
int mid = l + r >> 1;
if(loc <= mid) Insert(son[p][0], l, mid, loc);
else Insert(son[p][1], mid + 1, r, loc);
PushUp(p);
}
int Merge(int u, int v, int l = 1, int r = n) {
if(!u || !v) return u + v;
if(l == r) {
sum[u] = sum[u] + sum[v];
DelNode(v);
return u;
}
int mid = l + r >> 1;
son[u][0] = Merge(son[u][0], son[v][0], l, mid);
son[u][1] = Merge(son[u][1], son[v][1], mid + 1, r);
DelNode(v);
PushUp(u);
return u;
}
void Split(int u, int& v, int k, int flag) { //把u节点分裂,得到新的放到v里é¢ï¼Œåˆ†è£‚å‰k个数的节点
if(!k) return ;
v = NewNode();
if(k >= sum[ son[u][flag] ].sze) {
Split(son[u][flag ^ 1], son[v][flag ^ 1], k - sum[ son[u][flag] ].sze, flag);
swap(son[u][flag], son[v][flag]);
} else Split(son[u][flag], son[v][flag], k, flag);
PushUp(u), PushUp(v);
}
}S;
void Modify(int loc, int l, int r, int p) {
if(l == r) {
tree[p] = S.sum[ S.root[loc] ];
if(Op[loc]) swap(tree[p].ltype, tree[p].rtype);
return ;
}
int mid = l + r >> 1;
if(loc <= mid) Modify(loc, l, mid, p << 1);
else Modify(loc, mid + 1, r, p << 1 | 1);
tree[p] = tree[p << 1] + tree[p << 1 | 1];
}
Node Query(int nl, int nr, int l, int r, int p) {
if(nl <= l && nr >= r) return tree[p];
int mid = l + r >> 1;
if(nl > mid) return Query(nl, nr, mid + 1, r, p << 1 | 1);
else if(nr <= mid) return Query(nl, nr, l, mid, p << 1);
else return Query(nl, nr, l, mid, p << 1) + Query(nl, nr, mid + 1, r, p << 1 | 1);
}
set<int>::iterator Split(int x) {
auto pos = Set.lower_bound(x), tmp = pos;
if(*pos == x) return pos;
pos--;
S.Split(S.root[*pos], S.root[x], *tmp - x, Op[*pos] ^ 1);
Modify(*pos, 1, n, 1);
Op[x] = Op[*pos];
Modify(x, 1, n, 1);
Set.insert(x);
return Set.lower_bound(x);
}
signed main()
{
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m; Set.insert(n + 1);
for(int i = 1; i <= n; i++) {
int x; cin >> x;
Set.insert(i);
S.Insert(S.root[i], 1, n, x);
Modify(i, 1, n, 1);
}
while(m--) {
int op, l, r; cin >> op >> l >> r;
auto pl = Split(l), pr = Split(r + 1);
if(op == 3) cout << Query(l, r, 1, n, 1).ans + 1 << "\n";
else {
for(auto i = ++pl; i != pr; i++) {
S.Merge(S.root[l], S.root[*i]);
S.root[*i] = 0;
Modify(*i, 1, n, 1);
}
Set.erase(pl, pr);
Op[l] = op - 1;
Modify(l, 1, n, 1);
}
}
return 0;
}
G. Lexicographical Maximum
①若前\(n-1\)位都为9则直接输出字符串
②若前\(n-1\)位有一位不为9则输出长度为\(n-1\),只包含\('9'\)的字符串
#include<bits/stdc++.h>
using namespace std;
string S;
int n;
signed main()
{
cin >> S;
n = S.length();
bool flag = 1;
for(int i = 0; i < n - 1; i++) flag &= (S[i] == '9');
if(flag) cout << S;
else { for(int i = 0; i < n - 1; i++) cout << '9'; }
return 0;
}
I. Chiitoitsu
发现答案不与麻将的具体花色有关,只与各类麻将的数量有关,且一类麻将最多不超过两个。
我们可以通过计算两个同类的麻将(对子)数量和只有一个的麻将(单牌)数量来得出答案。
又因为每次抽到麻将之后不能放回去,所以当前能抽到的麻将的数量也需要被考虑。
设\(f[i][j]\)为单牌数量为\(i\)个,当前能抽到的麻将的数量为\(j\)个。
根据贪心策略,我们需要尽量将单牌凑双。所以抽到麻将后我们有两种情况:
①抽到合适的麻将可以凑双,我们结束游戏或者扔掉一张单牌,符合条件的麻将共有\(3i\)个,状态转移到\(f[i-2][j-1]\)
②抽到不合适的麻将直接扔掉,此时状态为\(f[i][j-1]\)
此时有
边界条件有
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define pir make_pair
#define fi first
#define se second
using namespace std;
const int MAXN = 2e5 + 5;
const int MOD = (int)1e9 + 7;
vector<int> vec;
vector< vector<int> > st;
int cnt, f[99][9999];
map<int, int> cur;
string S;
int qpow(int a, int p) {
int res = 1;
while(p) {
if(p & 1) res = res * a % MOD;
a = a * a % MOD;
p >>= 1;
}
return res;
}
void Init() {
for(int i = 1; i <= 13; i += 2) {
if(i == 1) f[1][3] = 1;
else f[i][3 * i] = f[i - 2][3 * i - 1] + 1;
for(int j = 3 * i + 1; j <= 34 * 4; j++) {
f[i][j] = j + (j - 3 * i) * f[i][j - 1] % MOD;
if(i > 1) f[i][j] += 3 * i * f[i - 2][j - 1] % MOD;
f[i][j] = f[i][j] % MOD * qpow(j, MOD - 2) % MOD;
}
}
}
int Hash(char x, char y) { return x * 3570 + (y + 128); }
signed main()
{
Init();
int TT, cnt = 0; cin >> TT;
while(TT--) {
cout << "Case #" << ++cnt << ": ";
cin >> S;
cur.clear();
for(int i = 0; i < 26; i += 2) cur[ Hash(S[i], S[i + 1]) ] += 1;
int cnt1 = 0;
for(auto i : cur) cnt1 += (i.se == 1);
cout << f[cnt1][34 * 4 - 13] << "\n";
}
return 0;
}