牛客多校2024-6
A - Cake
(神金,害我调了一个半小时)
Alice 和 Bob 玩一个游戏。游戏分为2阶段。
阶段1:有一棵边权值为 \(0\) 或 \(1\) 的有根树,两人轮流走,Alice 先走,走到叶子就停下来。记录下经过边的权值形成一个字符串\(S\)
阶段2:Bob 将一个蛋糕切成 \(len(S)\) 块,块可以为空。然后遍历 \(S\) 的每个字符,如果为 \(0\) 就 Alice 选择一块蛋糕;如果为 \(1\) 就 Bob 选择一块蛋糕。
每个人都期望利益最大化,求 Bob 获得的蛋糕比例最大值。
有根树节点数 \(n \leq 2e5\)
首先考虑阶段2:
显然如果 \(S[0] = \ '1'\) ,Bob 能获得整块蛋糕。将这个性质推广下去,Bob 能够将蛋糕切成任意的 \(k (k \leq len(S))\) 块,从而使 \(S[k]\) 以后的字符串失去了意义。
这意味着 Bob 能够选择 \(S\) 中的一个前缀 \(S_{pre}\) 切蛋糕。然后考虑 \(k\) 块怎么切,容易发现均等切是最优的,因为在 Alice 先手的情况下更大的蛋糕块会被 Alice 拿走从而不利(Bob 先手的情况答案为1)。
然后考虑阶段1:
显然这是一个树上博弈,Alice 需要让任意 \(S_{pre}\) 中 \(1\) 的占比最大值最小,Bob 需要让 \(S_{pre}\) 中 \(1\) 的占比最大值最大。
设计dfs状态为dfs(e, cnt0, cnt1)
代表进行到节点e,完成subtree(e)的计算时该玩家(可由cnt0+cnt1的奇偶得出)的最优解,于是就做完了。
此外还要注意 Alice 在最优解仍然可能走向权值为 \(1\) 得边,从而使答案变大,因此在除了根节点的每个节点都应该尝试更新答案的最大值,而不是仅在 Bob 的回合尝试更新。
const int U = 5e5;
int head[U], val[U], to[U], nxt[U], tot;
void add(int x, int y, int z)
{
to[++tot] = y; val[tot] = z; nxt[tot] = head[x]; head[x] = tot;
}
bool vis[U];
double dfs(int e, int cnt0, int cnt1)
{
int t = (cnt0+cnt1+1)%2;
double ans = !t;
vis[e] = 1;
bool leaf = 1;
for(int g = head[e]; g; g = nxt[g])
{
int y = to[g], z = val[g];
if (vis[y]) continue;
leaf = 0;
if (t == z)
{
if (t == 1) ans = max(ans, dfs(y, cnt0, cnt1+1));
else ans = min(ans, dfs(y, cnt0+1, cnt1));
}
else
{
if (t == 1) ans = max(ans, dfs(y, cnt0+1, cnt1)); // 1100100
else ans = min(ans, dfs(y, cnt0, cnt1+1));
}
}
if (leaf) ans = (double)cnt1/(cnt0+cnt1);
else if (e-1) ans = min(ans, (double)cnt1/(cnt0+cnt1));
return ans;
}
int T = next();
while(T--)
{
int n = next();
tot = 0;
rep(i, 0, n+1) head[i] = vis[i] = 0;
rep(i, 0, n-1)
{
int u, v, k;
cin>>u>>v>>k;
add(u, v, k);
add(v, u, k);
}
cout<<fixed<<setprecision(12)<<dfs(1, 0, 0)<<endl;
}
B - Cake 2
给定正 \(n\) 边形,连接所有距离(两点之间最小边数)为 \(k\) 的顶点,求多边形内部被划分为多少区域。
\(n \in [4, 1e6], k \in [2, n-2]\)
画图可以得知:若 \(2k = n\),则答案为 \(n\);否则答案为\(n*min\{k, n-k\}+1\)。
证明直接咕了。
int n, k;
cin>>n>>k;
if (2*k == n) cout<<n<<endl;
else
{
if (k > n/2) cout<<n*(n-k)+1<<endl;
else cout<<n*k+1<<endl;
}
C - Cake 3
怎么是记几构造啊/jk
D - Puzzle: Wagiri
给定一张简单连同无向图,边有两种状态Lun
或Qie
。
现要求移除任意一些边使得图连通,同时Lun
边都在环上,Qie
边都不在环上,输出任意方案或无解。
\(n \leq 1e5, m \in [n-1, 2e5]\)
显然如果对一个图求e-DCC
即边双连通分量,所有的边要么在一个e-DCC
中要么作为桥。
因此可以忽略所有Qie
边,对仅有Lun
边的图求e-DCC
,得出的所有桥删除后就能够满足所有Lun
边都在环上的条件。
然后考虑图联通这一条件,显然需要恢复几条Qie
边使各个完全不连通的e-DCC
连通。于是处理出所有Qie
边连接的两个e-DCC
,并使用并查集逐步尝试恢复Qie
边使图连通。因为并查集具有树状结构,所以保证了Qie
边都不在环上。
const int U = 5e5;
int head[U], to[U], nxt[U], stat[U], tot = 1;
void add(int x, int y, int st)
{
to[++tot] = y; nxt[tot] = head[x]; head[x] = tot; stat[tot] = st;
}
int dfn[U], low[U], n, m, ver;
bool bridge[U];
void tarjan(int e, int ine) // 求e-DCC
{
dfn[e] = low[e] = ++ver;
for(int g = head[e]; g; g = nxt[g])
{
if (stat[g]) continue;
int y = to[g], cc = 0;
if (!dfn[y])
{
tarjan(y, g);
low[e] = min(low[e], low[y]);
if (dfn[e] < low[y]) bridge[g] = bridge[g^1] = 1;
}
else if (g != (ine^1)) low[e] = min(low[e], dfn[y]);
}
}
int col[U], dcc;
void dfs(int e) // 编号e-DCC
{
col[e] = dcc;
for(int g = head[e]; g; g = nxt[g])
{
int y = to[g];
if (col[y] || stat[g] || bridge[g]) continue;
dfs(y);
}
}
int father[U]; // 并查集
int find(int k)
{
if (father[k] == k) return k;
return father[k] = find(father[k]);
}
void uni(int a, int b)
{
father[find(a)] = find(b);
}
cin>>n>>m;
rep(i, 0, m)
{
int u, v; string s;
cin>>u>>v>>s;
if (s[0] == 'L') add(u, v, 0), add(v, u, 0);
else add(u, v, 1), add(v, u, 1);
}
hrp(i, 1, n) if (!dfn[i]) tarjan(i, 0);
hrp(i, 1, n) if (!col[i]) dcc++, dfs(i);
hrp(i, 0, dcc) father[i] = i;
set<pii> ans;
hrp(i, 1, n) for(int g = head[i]; g; g = nxt[g])
{
int y = to[g];
if (i > y) continue; // 由于建了双向边所以忽略一个方向的边
if (!stat[g] && !bridge[g]) ans.insert({i, y}); // Lun边不是桥
else if (stat[g] && find(col[i]) != find(col[y])) // Qie边作为桥
{
ans.insert({i, y});
uni(col[i], col[y]);
}
}
bool yes = 1;
hrp(i, 2, dcc) if (find(i-1) != find(i)) yes = 0;
if (!yes) cout<<"NO"<<endl;
else
{
cout<<"YES"<<endl<<ans.size()<<endl;
for(auto p:ans) cout<<p.first<<' '<<p.second<<endl;
}
E - Palindrome Counter
求所有长为 \(n\),字符集个数为 \(k\) 的字符串的本质不同回文子串长度之和。
F - Challenge NPC 2
(随机算法大失败)
给定一棵森林,求其补图的Hamilton
路径。
\(n \leq 5e5\)
首先如果该森林为菊花,也就是补图中有一个点度数为 \(0\),显然不可能有解。
然后考虑直接对该森林进行分析。对其中一棵树从任意节点开始求深度,能够发现由于树的性质:
- 相同深度的节点间没有边
- 只有相邻深度的节点间可能有边
于是在补图上存在性质:如果两个节点深度不相邻,那么它们之间一定有边。
同时由于原图上树与树之间不连通,补图上两棵树的任意节点间都存在边。于是可以令:一棵树的起始节点深度为上一颗树深度最大值+1。这样可以获得编号 \(1 \sim m\) 的若干节点。由于补图性质,可以构造出2->4->...->1->3->...
的顺序使所有点都连通起来。如果 \(m < 4\),则需要特殊判定。
因为从任意的节点开始求深度并不总能获得深度的最大值,所以最终得出的总深度 \(m\) 可能小于 \(4\),从而造成错误的无解。于是应该对每棵树都从直径的一端开始求深度,从而能保证 \(m\) 取得最大,这样 \(m < 4\) 的情况只在 \(n < 4\) 时出现。
const int U = 2e6;
int tot, head[U], nxt[U], to[U];
void add(int x, int y)
{
to[++tot] = y; nxt[tot] = head[x]; head[x] = tot;
}
int dep[U];
vector<int> vd[U];
int T;
cin>>T;
while(T--)
{
int n, m;
cin>>n>>m;
hrp(i, 0, 3*n) head[i] = dep[i] = 0;
rep(i, 0, m)
{
int u, v; cin>>u>>v;
add(u, v); add(v, u);
}
if (n == 2)
{
if (m) cout<<-1<<endl;
else cout<<"1 2"<<endl;
continue;
}
if (n == 3)
{
if (m == 2) cout<<-1<<endl;
else if (m == 1)
{
int t;
hrp(i, 1, n) if (!head[i]) t = i;
if (t == 2) cout<<"1 2 3"<<endl;
else if (t == 1) cout<<"2 1 3"<<endl;
else cout<<"1 3 2"<<endl;
}
else cout<<"1 2 3"<<endl;
continue;
}
int maxd = 0;
hrp(i, 1, n) if (!dep[i])
{
queue<pii> que; que.push({i, 0});
int s, e;
while(que.size())
{
auto [x, fa] = que.front(); que.pop();
s = x;
for(int g = head[x]; g; g = nxt[g])
{
int y = to[g];
if (y != fa) que.push({y, x});
}
}
que.push({s, ++maxd});
while(que.size())
{
auto [x, d] = que.front(); que.pop();
dep[x] = maxd = d;
for(int g = head[x]; g; g = nxt[g])
{
int y = to[g];
if (!dep[y]) que.push({y, d+1});
}
}
}
hrp(i, 0, maxd) vd[i].clear();
hrp(i, 1, n) vd[dep[i]].pb(i);
if (maxd < 4) cout<<-1<<endl;
else
{
for(int d = 2; d <= maxd; d += 2) for(auto i:vd[d]) cout<<i<<' ';
for(int d = 1; d <= maxd; d += 2) for(auto i:vd[d]) cout<<i<<' ';
cout<<endl;
}
}
G - Easy Brackets Problem
/?
H - Genshin Impact's Fault
模拟Genshin抽卡,卡分为3星,4星,A5星,B5星4种。每10抽不能只有3星,每90抽至少有1个5星,抽到的5星必须AB交替。
int T = next();
while(T--)
{
string str = next<string>();
bool no = 0;
if (str.size() >= 10) hrp(i, 0, str.size()-10)
{
bool all3 = 1;
rep(j, i, i+10) if (str[j] != '3') all3 = 0;
if (all3)
{
no = 1;
break;
}
}
if (str.size() >= 90) hrp(i, 0, str.size()-90)
{
bool no5 = 1;
rep(j, i, i+90) if (str[j] == '5' || str[j] == 'U') no5 = 0;
if (no5)
{
no = 1;
break;
}
}
string s5;
rep(i, 0, str.size()) if (str[i] == '5' || str[i] == 'U') s5 += str[i];
if (s5.size()) rep(i, 0, s5.size()-1) if (s5[i] != 'U' && s5[i+1] != 'U') no = 1;
if (no) cout<<"invalid"<<endl;
else cout<<"valid"<<endl;
}
I - Intersecting Intervals
给定一个数字矩阵,每一行选择一个区间,相邻行区间必须有交,求选中数字和的最大值。
\(n*m \leq 1e6\)
一眼dp,很容易想出状态dp(i, j)代表dp在第i行选了j后的最大值
。
有一个显然的转移枚举该行 \(k\),强制该行选 \(j \sim k\),但 \(j\) 前和 \(k\) 后的数字还可能被选使答案更大。因此用O(m)
复杂度预处理出每行的pre(k), suf(k)
,分别代表包含 \(k\) 选 \(k\) 前和 \(k\) 后一些序列的最大贡献。
于是有:
dp(i, k) = dp(i-1, j)+sum(j...k)+pre(j)+suf(k)(j<k)
dp(i, k) = dp(i-1, j)+sum(k...j)+pre(k)+suf(j)(j>k)
从而得出了O(mn^2)
的转移(其中sum, pre, suf数组均右移了1位
):
rep(j, 0, m) hrp(k, 0, j) dp[i][j] = max(dp[i][j], dp[i-1][k]+sum[j+1]-sum[k]+pre[k]+suf[j+2]);
rep(j, 0, m) rev(k, m-1, j) dp[i][j] = max(dp[i][j], dp[i-1][k]+sum[k+1]-sum[j]+pre[j]+suf[k+2]);
复杂度不够,于是需要优化,有两种办法(仅说明 \(j < k\) 情况):
- 关注定值:能够发现
sum[j+1]+suf[j+2]
为定值,于是提出定值并在转移 \(j\) 的时候保存已经遍历过的dp[i-1][k]+sum[k]+pre[k]
的最大值。 - 关注转移 \(j\) 过程:设
V=dp[i-1][p]+sum[p+1]-sum[k]+pre[k]+suf[p+2]
,\(j = 1 \sim p\) 时包含所有 \(V\) 取值的集合为 \(S_p\) 。能够发现V'=dp[i-1][p]+sum[p+2]-sum[k]+pre[k]+suf[p+3]=V+sum[p+1]-sum[p]+suf[p+3]-suf[p+2]=V+w[p+1]+suf[p+3]-suf[p+2]
,从而让所有 \(V'\) 和dp[i-1][p+1]+v[i][p+1]+pre[p+1]+suf[p+3]
一起构成了\(S_{p+1}\)。
代码采用的是优化2:
int T = next();
while(T--)
{
v.clear();
dp.clear();
int n, m;
cin>>n>>m;
rep(i, 0, n+5) v.pb(vector<int>()), dp.pb(vector<int>());
hrp(i, 1, n) rep(j, 0, m) v[i].pb(next());
rep(i, 0, n+5) rep(j, 0, m+5) dp[i].pb(i ? -INF : 0);
hrp(i, 1, n)
{
vector<int> sum{0}, pre{0}, suf{0};
rep(j, 0, m) sum.pb(sum.back()+v[i][j]);
rep(j, 0, m) pre.pb(max(pre.back()+v[i][j], 0LL));
rev(j, m-1, 0) suf.pb(max(suf.back()+v[i][j], 0LL));
reverse(suf.begin(), suf.end());
suf.insert(suf.begin(), 0);
pre.insert(pre.end(), 0);
int maxx = -INF;
rep(j, 0, m)
{
maxx += v[i][j]-suf[j+1]+suf[j+2];
maxx = max(maxx, dp[i-1][j]+v[i][j]+pre[j]+suf[j+2]);
dp[i][j] = maxx;
}
maxx = -INF;
rev(j, m-1, 0)
{
maxx += v[i][j]-pre[j+1]+pre[j];
maxx = max(maxx, dp[i-1][j]+v[i][j]+pre[j]+suf[j+2]);
dp[i][j] = max(dp[i][j], maxx);
}
}
int ans = -INF;
rep(j, 0, m) ans = max(ans, dp[n][j]);
cout<<ans<<endl;
}
优化1:
int maxx = -INF;
rep(j, 0, m)
{
maxx = max(maxx, -sum[j]+pre[j]+dp[i-1][j]);
dp[i][j] = max(dp[i][j], maxx+sum[j+1]+suf[j+2]);
}
maxx = -INF;
rev(j, m-1, 0)
{
maxx = max(maxx, sum[j+1]+suf[j+2]+dp[i-1][j]);
dp[i][j] = max(dp[i][j], maxx-sum[j]+pre[j]);
}
J - Stone Merging
有 \(n\) 堆石子,开始时每堆 \(1\) 个,每个石子被可重复编号。有 \(k-1\) 个机器编号为 \(2 \sim k\)。
编号为 \(g\) 的机器可以合并 \(g\) 堆石子;但如果这 \(g\) 堆石子中有至少 \(1\) 个编号为 \(g\) 的石子,该编号为 \(g\) 的机器会不能再使用,并将所有编号为 \(g\) 的石子提取出并放成一堆,把剩下的石子放成另一堆。
输出将所有石子合成一堆的方案或无解。
\(1 \leq k \leq n \leq 1e5\)
除了仅有 \(2\) 堆编号为 \(2\) 的石子的情况,\(2 \sim k\)的编号都在石子上出现过是无解的。这种情况先进行特判。
首先编号为 \(2\) 的机器人一定存在,而且功能十分强大,能够将所有非 \(2\) 石子都合成一堆。
讨论:
- 只存在编号为 \(2\) 的机器人。由于特判过,所以不可能存在编号为\(2\)的石子。全都合并即可。
- 存在编号为 \(t\) 的机器人,使得编号为 \(t\) 的石头不存在。所以当前的任务为把石子堆数从 \(n\) 减少到 \(t\)(官方题解采用方法不同)。因为 \(3\) 号机器人存在,所以我们可以用 \(3\) 号机器人把编号为 \(2\) 的石子堆成双减少;同时用 \(2\) 号机器人把编号不为 \(2\) 的石子堆逐个减少。此外还可以在最后使用 \(2\) 号机器人把编号为 \(2\) 的石子合并为 \(1\) 堆。如果石子堆个数无法达到 \(t\),则无解(事实上一定有解)。
使用set
实现而非vector
来降低删除操作的时间复杂度。
官方题解的方法为第一步将\(y=(n−1)~mod~(x−1)+1\) 个石子合成成一堆, 后面每次都使用机器 \(t\) 来合成。
const int U = 1e6;
int w[U];
bool exist[U];
int T = next();
while(T--)
{
int n, k, op = 0;
cin>>n>>k;
hrp(i, 1, n) exist[i] = 0;
set<int> p2, pn2;
vector<vector<int>> ans;
hrp(i, 1, n)
{
cin>>w[i];
if (w[i] == 2) p2.insert(i);
else pn2.insert(i);
exist[w[i]] = 1;
}
if (!pn2.size() && p2.size() == 2)
{
cout<<"1"<<endl<<"2 1 2"<<endl;
continue;
}
int tar = 0;
rev(i, k, 2) if (!exist[i])
{
tar = i;
break;
}
if (!tar)
{
cout<<-1<<endl;
continue;
}
if (k == 2) while(pn2.size() > 1)
{
vector<int> v{*pn2.begin(), *next(pn2.begin())};
ans.pb(v);
pn2.erase(pn2.begin());
pn2.erase(pn2.begin());
op++;
pn2.insert(n+2*op);
}
else
{
bool mat = 0;
if (p2.size()+pn2.size() == tar) mat = 1;
while(pn2.size()+p2.size() > tar+1 && p2.size() > 2 && !mat)
{
vector<int> v{*p2.begin(), *next(p2.begin()), *next(next(p2.begin()))};
ans.pb(v);
p2.erase(p2.begin());
p2.erase(p2.begin());
p2.erase(p2.begin());
op++;
p2.insert(n+2*op);
if (p2.size()+pn2.size() == tar) mat = 1;
}
while(pn2.size() > 1 && !mat)
{
vector<int> v{*pn2.begin(), *next(pn2.begin())};
ans.pb(v);
pn2.erase(pn2.begin());
pn2.erase(pn2.begin());
op++;
pn2.insert(n+2*op);
if (p2.size()+pn2.size() == tar) mat = 1;
}
if (p2.size()+pn2.size() == tar+1 && p2.size() > 1)
{
vector<int> v{*p2.begin(), *next(p2.begin())};
ans.pb(v);
p2.erase(p2.begin());
p2.erase(p2.begin());
op++;
p2.insert(n+2*op-1);
mat = 1;
}
if (mat)
{
vector<int> v;
for(auto i:p2) v.pb(i);
for(auto i:pn2) v.pb(i);
ans.pb(v);
}
// else
// {
// cout<<-1<<endl;
// continue;
// }
}
cout<<ans.size()<<endl;
for(auto i:ans)
{
cout<<i.size()<<' ';
for(auto j:i) cout<<j<<' ';
cout<<endl;
}
}
K - The Great Wall 2
给定长为 \(n\) 的整数序列,要求分为恰好 \(k\) 个非空连续段使得这 \(k\) 段的极差之和最小。
要求对 \(k = 1 \sim n\) 分别求解
\(n \leq 5000\)
不会,zzz