Codeforces Round 1020 div3 个人题解(A~G2)
Dashboard - Codeforces Round 1020 (Div. 3) - Codeforces
A. Dr. TC
题目大意
给定一个01字符串 \(s\) ,由这个字符串生成一个字符串数组,字符串数组中第 \(i\) 个字符串即对 \(s\) 的第 \(i\) 个字符进行01翻转 后得到的字符串,问字符串数组中有多少个1。
解题思路
我们先统计出原始串中的0和1的数量 \(cnt0,cnt1\) ,答案即为 \(cnt0\times(cnt1+1)+cnt1 \times(cnt1-1)\) 。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
void solve()
{
int n;
cin >> n;
string s;
cin >> s;
int cnt1 = 0, cnt0 = 0;
for (auto &c : s)
{
cnt1 += (c == '1');
cnt0 += (c == '0');
}
cout << cnt0 * (cnt1 + 1) + cnt1 * (cnt1 - 1) << "\n";
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
B. St. Chroma
题目大意
给你一个长度为 \(n\) 的排列 \(p\),元素是 \(0\) 到 \(n-1\)。定义第 \(i\) 个前缀的上色值为 \(\mathrm{MEX}(p_1,\dots,p_i)\)。现在希望选一个喜欢的颜色 \(x\),构造一个排列,使得在所有前缀中涂成颜色 \(x\) 的格子数量最大化。
解题思路
要使第 \(i\) 个前缀的上色为 \(\mathrm{MEX}=x\),这个前缀中必须恰好包含 \(0,1,\dots,x-1\) ,否则小于 \(x\) 的某个数没出现,MEX 会更小,且不包含 \(x\),否则 MEX 会大于 \(x\) 。
令 \(L\) 为 \(0,1,\dots,x-1\) 中最后一个出现的位置,令 \(P\) 为元素 \(x\) 在全排列中的位置,则对于所有 \(i\) 满足 \(L \le i < P\) 时,前缀 \([1..i]\) 恰好包含 \({0,\dots,x-1}\) 且不含 \(x\),因而 \(\mathrm{MEX}=x\)。所以可获得的次数为 \(\max(0,P-L)\)。要最大化 \(P-L\),就应当让所有 \(0,\dots,x-1\) 尽可能集中在最前面,让 \(x\) 放到 \(n\) 。这样 \(L=x\),\(P=n\),获得次数 \(n-x\),已是可行的最大值。
当 \(x=0\) 时,前缀要 MEX=0,必须不含 0,直到看到 0 前的所有前缀都算。最优做法是把 0 放在最后,得到次数 \(n-1\) 。
当 \(x=n\) 时,MEX 想要 \(n\),只有在前缀包含了所有 \(0\ldots n-1\) 时才行,即只有完整前缀长度 \(n\) 一次。此时任意排列都能达到最多一次。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
void solve()
{
int n, x;
cin >> n >> x;
if (x == 0)
{
for (int i = 1; i < n; i++)
{
cout << i << " ";
}
cout << 0 << "\n";
return;
}
if (x == n)
{
for (int i = 0; i < n; i++)
{
cout << i << " \n"[i == n - 1];
}
return;
}
for (int i = 0; i < x; i++)
{
cout << i << " ";
}
for (int i = x + 1; i < n; i++)
{
cout << i << " ";
}
cout << x << "\n";
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
C. Cherry Bomb
题目大意
给定两个长度为 \(n\) 的整型数组 \(a\) 和 \(b\),其中 \(a_i\) 和 \(b_i\) 的取值范围为 \([0, k]\),且有些 \(b_i\) 丢失,用 \(-1\) 表示。我们称 \(a\) 和 \(b\) 是互补的,当且仅当存在一个常数 \(x\),使得对所有 \(1\le i\le n\) 都有\(a_i + b_i = x\),现在要为所有丢失的 \(b_i\) 填上一个值,使得填完后数组 \(b\) 与 \(a\) 互补。问有多少种方案。
解题思路
遍历一遍 \(a,b\) ,如果 \(b_i \neq -1\),则可以算出一个候选值 \(x = a_i + b_i\) 。若后续出现不同的 \(a_j + b_j\neq x\),则不可能互补,答案为 0,如果确定了唯一的 \(x\),则对每个缺失位置需要填入 \(x - a_i\) ( \(0\le x-a_i\le k\) ),若都满足,则方案数为 1。
如果所有 \(b_i=-1\),那么 \(x\) 可以在一个区间内任意取值。对 \(b_i\) 来说, \(0 \le x - a_i \le k \quad\Longrightarrow\quad a_i \le x \le a_i + k\) 。答案即为$ \min{a_i} -\max{a_i}+ k+1$。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
void solve()
{
int n;
ll k;
cin >> n >> k;
vl a(n);
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
vl b(n);
for (int i = 0; i < n; i++)
{
cin >> b[i];
}
ll x = -1;
for (int i = 0; i < n; i++)
{
if (b[i] != -1)
{
ll cur = a[i] + b[i];
if (x == -1)
{
x = cur;
}
else if (x != cur)
{
cout << 0 << "\n";
return;
}
}
}
if (x != -1)
{
for (int i = 0; i < n; i++)
{
if (b[i] == -1)
{
ll need = x - a[i];
if (need < 0 || need > k)
{
cout << 0 << "\n";
return;
}
}
}
cout << "1\n";
return;
}
ll mx = 0, mn = infll;
for (int i = 0; i < n; i++)
{
mx = max(mx, a[i]);
mn = min(mn, a[i] + k);
}
ll ans = 0;
if (mx <= mn)
ans = mn - mx + 1;
cout << ans << "\n";
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
D. Flower Boy
题目大意
给两个序列长度为 \(n\) 和 \(m\) 的序列 \(a,b\) ,要求在 \(a\) 中恰好选出一个长度为 \(m\) 的子序列 \(c\),使得\(c_i \ge b_i,\quad i=1,2,\dots,m\)
在选之前,允许你在 \(a\) 的任意位置插入恰好一个值为 \(k\) 的元素,问最小的 \(k\) 应取何值,使得上述选取可行;若原序列已可满足,输出 \(0\);若无论 \(k\) 取何值都不可行,输出 \(-1\)。
解题思路
我们用 \(pre[i]\) 表示在不插花的情况下,只看原序列前 \(i\) 个花,最多能匹配需求数组 \(b\) 的前多少个:
我们用 \(suf[i]\) 表示只看原序列从第 \(i\) 个到末尾,最多能从 \(b\) 的末尾匹配多少个:
这样我们就能快速知道,在插入点右侧还能匹配需求的多少尾部。
枚举所有插入点,计算最小 \(k\)
插在第 \(i\) 和第 \(i+1\) 个花之间,前半段最多已匹配 \(p=\text{pre}[i]\) 个需求;若要让插入的那朵花去匹配需求的第 \(p+1\) 项,必须满足 \(k\ge b_{p+1}\) ,并且右侧还能匹配剩余 \(m-(p+1)\) ,即 \(\text{suf}[i+1]\ge m-p-1\) 。
遍历所有 \(i\in[0,n]\),取能满足的最小 \(b_{p+1}\) 即为答案。如果前缀本身就能匹配 \(m\) 个,则输出 0,遍历后没有可行解则输出 \(-1\)。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
void solve()
{
int n, m;
cin >> n >> m;
vl a(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
vl b(m + 1);
for (int i = 1; i <= m; i++)
{
cin >> b[i];
}
vl pre(n + 1);
for (int i = 1; i <= n; i++)
{
if (pre[i - 1] < m && a[i] >= b[pre[i - 1] + 1])
pre[i] = pre[i - 1] + 1;
else
pre[i] = pre[i - 1];
}
if (pre[n] >= m)
{
cout << 0 << "\n";
return;
}
vl suf(n + 2);
for (int i = n; i >= 1; i--)
{
if (suf[i + 1] < m && a[i] >= b[m - suf[i + 1]])
suf[i] = suf[i + 1] + 1;
else
suf[i] = suf[i + 1];
}
ll ans = infll;
for (int i = 0; i <= n; i++)
{
if (pre[i] >= m)
continue;
if (suf[i + 1] >= m - pre[i] - 1)
ans = min(ans, b[pre[i] + 1]);
}
if (ans == infll)
ans = -1;
cout << ans << "\n";
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
E. Wolf
题目大意
有一个长度为 \(n\) 的排列 \(p\) ,以及 \(q\) 次独立查询。每次查询给定区间 \([l,r]\) 和目标值 \(x\)。在对这个区间上模拟二分查找时,如果当前中点值与 \(x\) 比较的结果和数组实际顺序不一致,就会失败。允许在查询前任意选取 \(d\) 个 不是 \(x\) 的下标,将它们对应的值重新任意重排,来保证查找成功。求每次查询的最小 \(d\),若无法通过任何重排成功则输出 \(-1\)。
解题思路
阅读理解题,理解题意后不难。
首先预处理出每个值在排列中的位置,对 \(i=1,2,\dots ,n\) 记录 \(pos[i]\)。
对于每个查询 \(L,R,x\),令 \(k=\text{pos}[x]\),若 \(k\notin[L,R]\) 则直接输出 \(-1\),否则在区间 \([l=L,r=R]\) 上模拟二分查找,维护4个值:
\(cnt0\) :访问到且无需替换的 \(p[mid]<x\) 次数
\(cnt1\) :访问到且无需替换的 \(p[mid]>x\) 次数
\(cntL\) :必须将 \(p[mid]\) 当作 > \(x\) 来替换的次数
\(cntR\) :必须当作 < \(x\) 来替换的次数
令 \(l=L,R=r,mid=\frac{l+r}{2}\)
- 当 \(mid \lt k\) 且 \(p[mid]\lt x\) 时,向右搜索,累加小于 \(x\) 的访问数 \(cnt0+1\) 。
- 当 \(mid<k\) 且 \(p[mid]>x\) 时,本来会向右但 \(p[mid]\) 又不小于 \(x\),因此必须把它当成大于 \(x\) 来替换,\(cntL+1,cnt1+1\) ,并令 \(l=mid+1\) 。
- 当 \(mid>k\) 且 \(p[mid]>x\) 时,向左搜索,累加大于 \(x\) 的访问数 \(cnt1+1\) 。
- 当 \(mid>k\) 且 \(p[mid]<x\) 时,本来会向左但 \(p[mid]\) 又不大于 \(x\),因此必须把它当成小于 \(x\) 来替换, \(cntR+1,cnt0+1\) ,并令 \(r=mid-1\)。
当 \(mid=k\) 停止。此时 \(cntL\) 是需要替换成大于 \(x\) 的次数,\(cntR\) 是需要替换成小于 \(x\) 的次数,\(cnt0\) / \(cnt1\) 分别是整个过程中访问到的小于/大于 \(x\) 的点数。可用的小于 \(x\) 的数目为 \((x-1)-cnt0\),可用的大于 \(x\) 的数目为 \((n-x)-cnt1\);若它们无法分别覆盖 \(\max(0,cntR-cntL)\) 或 \(\max(0,cntL-cntR)\) 这两种多余替换需求,就输出 \(-1\),否则最少替换次数为 \(2\max(cntL,cntR)\)。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
void solve()
{
int n, q;
cin >> n >> q;
vi p(n + 1);
vi pos(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> p[i];
pos[p[i]] = i;
}
while (q--)
{
int L, R, x;
cin >> L >> R >> x;
int k = pos[x];
if (k < L || k > R)
{
cout << "-1 ";
continue;
}
int l = L, r = R;
int cntL = 0, cntR = 0, cnt0 = 0, cnt1 = 0;
vector<bool> vis(n + 1);
while (l <= r)
{
int mid = (l + r) >> 1;
vis[mid] = true;
if (mid == k)
break;
if (mid < k)
{
if (p[mid] < x)
cnt0++;
else
cntL++, cnt1++;
l = mid + 1;
}
else
{
if (p[mid] > x)
cnt1++;
else
cntR++, cnt0++;
r = mid - 1;
}
}
if (x - 1 - cnt0 < max(0, cntL - cntR) || n - x - cnt1 < max(0, cntR - cntL))
cout << "-1 ";
else
cout << 2 * max(cntL, cntR) << " ";
}
cout << '\n';
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
F. Goblin
题目大意
给定二值向量
构造二值矩阵 \(G\in\{0,1\}^{n\times n}\):
视矩阵中值为 0 的单元格以四连通(上下左右)方式连通,求这些连通分量中的最大的联通分量大小。
解题思路
对于每个下标 \(j\) 满足 \(s_j=0\),将该列分为三部分:
- 上半段:所有 \(G_{i,j}\) 使 \(1\le i\lt j\);共 \(j-1\) 个格子,记作节点 \(U_j\) ,联通块大小为 \(j-1\) 。
- 下半段:所有 \(G_{i,j}\) 使 $ j$;共 \(n-j\) 个格子,记作节点 \(D_j\) ,联通块大小为 \(n-j\) 。
对于每个下标 \(j\) 满足 \(s_j=1\) , \(G_{j,j}\) 为零,记作节点 \(T_j\),联通块大小为 1。
对于每个 \(1\le j\lt n\),若 \(s_j=s_{j+1}=0\),则:
- 合并 \(U_j\) 与 \(U_{j+1}\)(它们在行方向上上下段相连)
- 合并 \(D_j\) 与 \(D_{j+1}\) (同理)
对每个 \(j\) 使 \(s_j=1\):
- 若 \(j>1\) 且 \(s_{j-1}=0\),合并 \(T_j\) 与 \(D_{j-1}\) , \(G_{j,j}\) 可向左下连通。
- 若 \(j\lt n\) 且 \(s_{j+1}=0\),合并 \(T_j\) 与 \(U_{j+1}\) , \(G_{j,j}\) 可向右上连通。
所有列遍历合并后,遍历所有联通分量,联通分量大小最大值即为答案。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
struct DSU
{
vector<int> f; // 父节点
vector<ll> siz; // 按秩(近似)维护子树大小,用于合并优化
DSU() {}
DSU(int n) { init(n); }
// 初始化并查集,n 为节点总数
void init(int n)
{
f.resize(n);
iota(f.begin(), f.end(), 0);
siz.assign(n, 1);
}
// 路径压缩查找根
int find(int x)
{
while (x != f[x])
x = f[x] = f[f[x]];
return x;
}
// 合并两个集合,并把权重累加到新的根
bool merge(int x, int y)
{
x = find(x);
y = find(y);
if (x == y)
return false;
// 保证 siz[x] >= siz[y]
if (siz[x] < siz[y])
swap(x, y);
f[y] = x;
siz[x] += siz[y];
return true;
}
ll size(int x)
{
return siz[find(x)];
}
};
void solve()
{
int n;
string s;
cin >> n >> s;
vi U(n, -1);
vi D(n, -1);
vi T(n, -1);
int idx = 0;
for (int j = 0; j < n; j++)
{
if (s[j] == '0')
{
U[j] = idx++;
D[j] = idx++;
}
else
{
T[j] = idx++;
}
}
DSU dsu(idx);
for (int i = 0; i < n; i++)
{
if (s[i] == '0')
{
dsu.siz[U[i]] = i;
dsu.siz[D[i]] = n - i - 1;
}
}
for (int i = 0; i + 1 < n; i++)
{
if (s[i] == '0' && s[i + 1] == '0')
{
dsu.merge(U[i], U[i + 1]);
dsu.merge(D[i], D[i + 1]);
}
}
for (int i = 0; i < n; i++)
{
if (s[i] == '1')
{
int x = T[i];
if (i - 1 >= 0 && s[i - 1] == '0')
dsu.merge(x, D[i - 1]);
if (i + 1 < n && s[i + 1] == '0')
dsu.merge(x, U[i + 1]);
}
}
ll ans = 0;
for (int i = 0; i < idx; i++)
{
if (dsu.find(i) == i)
ans = max(ans, dsu.size(i));
}
cout << ans << "\n";
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
G1. Baudelaire (easy version)
题目大意
给定一棵特殊的树(简单版本条件)树,树中每个节点都与节点 1 相连,但节点 1 不一定是根,共有 \(n\) 个节点。每个节点有一个初始权值,只有两种可能: \(+1\) 或 \(-1\)。
树有一个隐藏的根节点 \(\text{rt}\),定义 \(f(u)=\sum_{x\in\text{rt}\rightsquigarrow u} \text{val}[x]\) ,即为从根到节点 \(u\) 路径上所有节点权值之和。
你可以进行两种交互式操作,总次数不能超过 \(n+200\):
? 1 k a_1 a_2 … a_k,会返回 \(f(a_1)+f(a_2)+…+f(a_k)\) 。? 2 u,将节点 \(u\) 的权值从 \(+1\) ↔ \(-1\) 互换,为永久修改。
在交互结束后,你要输出所有节点最终的权值。
解题思路
询问? 1 1 1 得到\(F_0=f(1).\)
接着对每个 \(v=2,3,\dots,n\) 分别询问 ? 1 1 v 得到 \(F_0(v)=f(v).\)
由于树为星形,类似菊花图,如果 \(v\neq\mathrm{rt}\),必有 \(f(v)=f(1)+\mathrm{val}(v) \Longrightarrow\mathrm{val}(v)=F_0(v)-F_0.\) ,当 \(v=\mathrm{rt}\) 时上述等式不成立,需要后续操作加以区分。
发送? 2 1,将节点 1 的权值取反,再次发送? 1 1 1得到\(F_1=f'(1).\)
设翻转前 \(\mathrm{val}(1)=x\),隐藏根的权值为 \(\mathrm{val}(\mathrm{rt})\),则 \(F_0=\mathrm{val}(\mathrm{rt})+x, F_1=\mathrm{val}(\mathrm{rt})-x \Longrightarrow x=\frac{F_0-F_1}{2}\) ,此时节点 1 的最终权值为 \(-x\)。
如果 \(F_0 = x\) , 则隐藏根即为 1,此时直接令\(\mathrm{val}(1)=-x, \mathrm{val}(v)=F_0(v)-F_0(v\ge2)\) 。
如果 \(F_0 \neq x\) , 则隐藏根即不为 1,它在集合 \({2,3,\dots,n}\) 中。
假设对叶子进行翻转,那么路径和应该为 \(\mathrm{pre}(v)=F_0(v)-2x.\)
令候选集合 \(C=\{x_i\}\),初始时 \(x_i=i+1,1\le i \le n-1\) 。
进行二分,取一半候选集合为 \(S=\{x_i\} ,1\le i\lt \frac{|C|}{2}\) ,另一半为 $T={x_i},\frac{|C|}{2}\le i\le |C| $
询问? 1 |S| S,得到实测总和 \(real\),计算预测总和 \(ex=\sum_{v\in S}f(v)\) 。
如果 \(real-ex=2x\) , 则根在集合 \(S\),否则在 \(T\)。更新 \(C\) 为对应子集,直到 \(|C|=1\) 。此时唯一元素即为隐藏根,再令 \(\mathrm{val}(\mathrm{rt})=F_0-x\) , 连同先前存储的其他节点 \(\mathrm{val}(v)\) 一并输出。
最多查询次数 \(1+(n-1)+1+1+\lceil\log_2(n-1)\rceil \le n+12 < n+200\) 。
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
ll op1(const vi &v)
{
int n = v.size();
cout << "? 1 " << n << " ";
for (int i = 0; i < n; i++)
{
cout << v[i] << " \n"[i == n - 1];
}
cout << flush;
ll res;
cin >> res;
return res;
}
void op2(int u)
{
cout << "? 2 " << u << "\n";
cout << flush;
}
void solve()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
}
ll f0 = op1({1});
vl f(n + 1);
for (int i = 2; i <= n; i++)
{
f[i] = op1({i});
}
op2(1);
ll f1 = op1({1});
ll x = (f0 - f1) / 2;
vl ans(n + 1);
ans[1] = -x;
for (int i = 2; i <= n; i++)
{
ans[i] = f[i] - f0;
}
if (f0 == x)
{
cout << "! ";
for (int i = 1; i <= n; i++)
{
cout << ans[i] << " \n"[i == n];
}
cout << flush;
return;
}
vi C;
for (int i = 2; i <= n; i++)
{
C.push_back(i);
}
auto calc = [&](int v) -> ll
{
return f[v] - 2 * x;
};
while (C.size() > 1)
{
int m = C.size() / 2;
vi S(C.begin(), C.begin() + m);
ll sum = 0;
for (auto &x : S)
{
sum += calc(x);
}
ll aim = S.empty() ? 0 : op1(S);
if (aim == sum)
C.erase(C.begin(), C.begin() + m);
else
C.resize(m);
}
int rt = C[0];
ans[rt] = f0 - x;
cout << "! ";
for (int i = 1; i <= n; i++)
{
cout << ans[i] << " \n"[i == n];
}
cout << flush;
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
G2. Baudelaire (hard version)
题目大意
与简单版本相比,树不再有特殊形式,不一定所有节点都与点 1 相连,其它不变。
解题思路
其实大体上与简单版本思路相同,只不过增加了一步重心分治
整套方案与简单版本一样分为两部分:确定根、恢复权值。
先解释单个节点 \(u\) 的“邻居二分”原理。设 \(p_u\) 为父亲,其余相邻节点记作 \(c_1,c_2,\dots,c_k\)。翻转 \(u\) 后,父亲的路径和保持 \(s_{p_u}\) 不变,而每个非父亲邻居的路径和统一变化 \(\pm2\),于是有
\(s_{c_i}'-s_{c_i}= \pm2 \quad(i\ne p_u),\quad s_{p_u}'-s_{p_u}=0.\)
若一次查询某个邻居子集 \(\Sigma\) 的路径和并记为 \(S_1\),翻转 \(u\) 再查询同一子集得到 \(S_2\),则差值满足
\(|S_1-S_2|= \begin{cases} 2|\Sigma|,& p_u\notin \Sigma,\\ <2|\Sigma|,& p_u\in \Sigma. \end{cases}\)
因此用二分法:把邻居按任意顺序分成左右两段,三次查询(求和、翻转、再求和)即可判断父亲在哪一侧,重复 \(\lceil\log_2 k\rceil\) 轮后得到唯一父亲;若过程中发现邻居集合为空,则 \(u\) 本身即为根。一次找父亲总查询次数为 \(3\log k\)。
要保证总查询量不超标,需要在小而优的节点上调用“找父亲”。树的重心 \(c\) 具有删除后所有连通块大小 \(\le\frac{|S|}{2}\) 的性质,其中 \(|S|\) 是当前候选根集合大小。取当前候选集合的重心 \(c\) 并对 \(c\) 执行上述二分:
若找不到父亲,则 \(c\) 就是真根;若找到父亲 \(p_c\),真实根必位于包含 \(p_c\) 的那一侧,只需一次 DFS 将另一侧整块节点标记为非候选。候选集合大小至少减半,所以重心过程至多进行 \(\lceil\log_2 n\rceil\) 轮;每轮用到的查询数上界为 \(3\log(\deg c)\),当 \(n\le1000\) 时总计严格小于 \(165\) 。
根 \(rt\) 确定后,再对每个结点单点查询一次? 1 1 u即可得到所有路径和 \(s_u\),共用 \(n\) 次查询。随后 DFS 构建父子关系并利用\(v_r=s_r,\quad v_u=s_u-s_{p_u}\quad(u\ne r)\) ,就能还原整棵树的最终取值,它们必然落在 \({+1,-1}\),因为对真实父子边 \((p,u)\) 有\(s_u-s_{p_u}=v_u\in\{\pm1\}\) 。
总的查询复杂度为 \(n+165<n+200\)
代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vl = vector<ll>;
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
ll op1(const vi &v)
{
cout << "? 1 " << v.size() << ' ';
for (int i = 0; i < (int)v.size(); i++)
{
cout << v[i] << " \n"[i + 1 == (int)v.size()];
}
cout << flush;
ll res;
cin >> res;
return res;
}
void op2(int u)
{
cout << "? 2 " << u << '\n'
<< flush;
}
void solve()
{
int n;
cin >> n;
vector<vi> adj(n + 1);
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
vector<bool> sta(n + 1, true);
vi siz(n + 1);
// DFS 计算子树大小
auto dfs1 = [&](auto self, int u, int fa) -> void
{
siz[u] = 1;
for (auto &v : adj[u])
{
if (v == fa)
continue;
if (sta[v])
{
self(self, v, u);
siz[u] += siz[v];
}
}
};
// 寻找重心,先算子树大小,随后不断走向“超半子树”
auto findCentr = [&](int u) -> int
{
dfs1(dfs1, u, 0);
int tot = siz[u];
int res = u, pa = 0;
bool moved = true;
while (moved)
{
moved = false;
for (auto &v : adj[res])
{
if (v != pa && sta[v] && siz[v] * 2 > tot)
{
pa = res;
res = v;
moved = true;
break;
}
}
}
return res; // cur 即重心
};
auto hasParent = [&](const vi &seg, int u) -> bool
{
if (seg.empty())
return false;
ll before = op1(seg);
op2(u); // 翻转一次
ll after = op1(seg);
return llabs(before - after) < 2LL * seg.size();
};
// 二分定位 u 的父亲(若返回 0 则 u 为根),每次判定只 3 次查询,父亲唯一,log(deg) 轮
auto findParent = [&](int u) -> int
{
vi nbr;
for (auto &v : adj[u])
{
if (!sta[v])
continue;
nbr.push_back(v);
}
if (nbr.empty())
return 0; // 没邻居 → 根
vi cand = nbr;
while (cand.size() > 1)
{
int m = cand.size() / 2;
vi left(cand.begin(), cand.begin() + m);
if (hasParent(left, u))
cand.erase(cand.begin() + m, cand.end()); // 父亲在左半
else
cand.erase(cand.begin(), cand.begin() + m); // 父亲在右半
}
/* 此时 cand.size() == 1 */
return hasParent({cand[0]}, u) ? cand[0] : 0;
};
// 从结点 bad 出发 DFS,把与 keep 不连通的部分.全部标记为“死”(sta=0)。
auto eraseSide = [&](int bad, int keep) -> void
{
vi stk{bad};
while (!stk.empty())
{
int x = stk.back();
stk.pop_back();
sta[x] = false; // 删除
for (auto &v : adj[x])
{
if (sta[v] && v != keep)
stk.push_back(v);
}
}
};
// 重心分治 + 二分找父 来锁定真实根, 最坏 query ≈ 3 Σ log(deg) ≤ 165
int rt = -1;
while (true)
{
int entry = int(find(sta.begin() + 1, sta.end(), true) - sta.begin());
int cen = findCentr(entry); // 当前重心
int par = findParent(cen); // 父亲 (0 ⇒ cen 为根)
if (!par)
{
rt = cen;
break;
} // 已确定根
eraseSide(cen, par); // 只保留父亲那一侧
}
// 单点查询所有 sum_u , n 次查询
vl sum(n + 1);
for (int i = 1; i <= n; i++)
{
sum[i] = op1({i});
}
// DFS得到 父节点p数组
vi p(n + 1);
auto dfs2 = [&](auto self, int u, int fa) -> void
{
p[u] = fa;
for (auto &v : adj[u])
{
if (v == fa)
continue;
self(self, v, u);
}
};
dfs2(dfs2, rt, 0);
// 根据前缀和差分出结点实际取值
vi ans(n + 1);
ans[rt] = sum[rt];
for (int i = 1; i <= n; i++)
{
if (i == rt)
continue;
ans[i] = sum[i] - sum[p[i]];
}
cout << "! ";
for (int i = 1; i <= n; i++)
{
cout << ans[i] << " \n"[i == n];
}
cout << flush;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int _ = 1;
cin >> _;
while (_--)
solve();
return 0;
}
/*######################################END######################################*/
// 链接:
G题挺有意思的,一开始想直接开G2,想了一会没思路,然后回头开G1,模拟玩了一下,想到可以集合二分判根,写了一下就过了。挺有意思的交互题。

浙公网安备 33010602011771号