Codeforces Round 1020 div3 个人题解(A~G2)

Codeforces Round 1020 div3 个人题解(A~G1)

Dashboard - Codeforces Round 1020 (Div. 3) - Codeforces


A. Dr. TC

题目大意

给定一个01字符串 s s s ,由这个字符串生成一个字符串数组,字符串数组中第 i i i 个字符串即对 s s s 的第 i i i 个字符进行01翻转 后得到的字符串,问字符串数组中有多少个1。

解题思路

我们先统计出原始串中的0和1的数量 c n t 0 , c n t 1 cnt0,cnt1 cnt0,cnt1 ,答案即为 c n t 0 × ( c n t 1 + 1 ) + c n t 1 × ( c n t 1 − 1 ) cnt0\times(cnt1+1)+cnt1 \times(cnt1-1) cnt0×(cnt1+1)+cnt1×(cnt11)

代码实现
#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 n n 的排列 p p p,元素是 0 0 0 n − 1 n-1 n1。定义第 i i i 个前缀的上色值为 M E X ( p 1 , … , p i ) \mathrm{MEX}(p_1,\dots,p_i) MEX(p1,,pi)。现在希望选一个喜欢的颜色 x x x,构造一个排列,使得在所有前缀中涂成颜色 x x x 的格子数量最大化。

解题思路

要使第 i i i 个前缀的上色为 M E X = x \mathrm{MEX}=x MEX=x,这个前缀中必须恰好包含 0 , 1 , … , x − 1 0,1,\dots,x-1 0,1,,x1 ,否则小于 x x x 的某个数没出现,MEX 会更小,且不包含 x x x,否则 MEX 会大于 x x x

L L L 0 , 1 , … , x − 1 0,1,\dots,x-1 0,1,,x1 中最后一个出现的位置,令 P P P 为元素 x x x 在全排列中的位置,则对于所有 i i i 满足 L ≤ i < P L \le i < P Li<P 时,前缀 [ 1.. i ] [1..i] [1..i] 恰好包含 0 , … , x − 1 {0,\dots,x-1} 0,,x1 且不含 x x x,因而 M E X = x \mathrm{MEX}=x MEX=x。所以可获得的次数为 max ⁡ ( 0 , P − L ) \max(0,P-L) max(0,PL)。要最大化 P − L P-L PL,就应当让所有 0 , … , x − 1 0,\dots,x-1 0,,x1 尽可能集中在最前面,让 x x x 放到 n n n 。这样 L = x L=x L=x P = n P=n P=n,获得次数 n − x n-x nx,已是可行的最大值。

x = 0 x=0 x=0 时,前缀要 MEX=0,必须不含 0,直到看到 0 前的所有前缀都算。最优做法是把 0 放在最后,得到次数 n − 1 n-1 n1

x = n x=n x=n 时,MEX 想要 n n n,只有在前缀包含了所有 0 … n − 1 0\ldots n-1 0n1 时才行,即只有完整前缀长度 n n 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 n n 的整型数组 a a a b b b,其中 a i a_i ai b i b_i bi 的取值范围为 [ 0 , k ] [0, k] [0,k],且有些 b i b_i bi 丢失,用 − 1 -1 1 表示。我们称 a a a b b b 是互补的,当且仅当存在一个常数 x x x,使得对所有 1 ≤ i ≤ n 1\le i\le n 1in 都有 a i + b i = x a_i + b_i = x ai+bi=x,现在要为所有丢失的 b i b_i bi 填上一个值,使得填完后数组 b b b a a a 互补。问有多少种方案。

解题思路

遍历一遍 a , b a,b a,b ,如果 b i ≠ − 1 b_i \neq -1 bi=1,则可以算出一个候选值 x = a i + b i x = a_i + b_i x=ai+bi 。若后续出现不同的 a j + b j ≠ x a_j + b_j\neq x aj+bj=x,则不可能互补,答案为 0,如果确定了唯一的 x x x,则对每个缺失位置需要填入 x − a i x - a_i xai 0 ≤ x − a i ≤ k 0\le x-a_i\le k 0xaik ),若都满足,则方案数为 1。

如果所有 b i = − 1 b_i=-1 bi=1,那么 x x x 可以在一个区间内任意取值。对 b i b_i bi 来说, 0 ≤ x − a i ≤ k ⟹ a i ≤ x ≤ a i + k 0 \le x - a_i \le k \quad\Longrightarrow\quad a_i \le x \le a_i + k 0xaikaixai+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 n n m m m 的序列 a , b a,b a,b ,要求在 a a a 中恰好选出一个长度为 m m m 的子序列 c c c,使得 c i ≥ b i , i = 1 , 2 , … , m c_i \ge b_i,\quad i=1,2,\dots,m cibi,i=1,2,,m

在选之前,允许你在 a a a 的任意位置插入恰好一个值为 k k k 的元素,问最小的 k k k 应取何值,使得上述选取可行;若原序列已可满足,输出 0 0 0;若无论 k k k 取何值都不可行,输出 − 1 -1 1

解题思路

我们用 p r e [ i ] pre[i] pre[i] 表示在不插花的情况下,只看原序列前 i i i 个花,最多能匹配需求数组 b b b 的前多少个:
pre [ 0 ] = 0 , pre [ i ] = { pre [ i − 1 ] + 1 , 若 pre [ i − 1 ] < m  且  a i ≥ b pre [ i − 1 ] + 1 , pre [ i − 1 ] , 否则. \text{pre}[0]=0,\quad \text{pre}[i]= \begin{cases} \text{pre}[i-1]+1, &\text{若 }\text{pre}[i-1]<m\text{ 且 }a_i\ge b_{\text{pre}[i-1]+1},\\ \text{pre}[i-1], &\text{否则.} \end{cases} pre[0]=0,pre[i]={pre[i1]+1,pre[i1], pre[i1]<m  aibpre[i1]+1,否则.
我们用 s u f [ i ] suf[i] suf[i] 表示只看原序列从第 i i i 个到末尾,最多能从 b b b 的末尾匹配多少个:
suf [ n + 1 ] = 0 , suf [ i ] = { suf [ i + 1 ] + 1 , 若 suf [ i + 1 ] < m  且  a i ≥ b m − suf [ i + 1 ] , suf [ i + 1 ] , 否则. \text{suf}[n+1]=0,\quad \text{suf}[i]= \begin{cases} \text{suf}[i+1]+1, &\text{若 }\text{suf}[i+1]<m\text{ 且 }a_i\ge b_{m-\text{suf}[i+1]},\\ \text{suf}[i+1], &\text{否则.} \end{cases} suf[n+1]=0,suf[i]={suf[i+1]+1,suf[i+1], suf[i+1]<m  aibmsuf[i+1],否则.
这样我们就能快速知道,在插入点右侧还能匹配需求的多少尾部。

枚举所有插入点,计算最小 k k k
插在第 i i i 和第 i + 1 i+1 i+1 个花之间,前半段最多已匹配 p = pre [ i ] p=\text{pre}[i] p=pre[i] 个需求;若要让插入的那朵花去匹配需求的第 p + 1 p+1 p+1 项,必须满足 k ≥ b p + 1 k\ge b_{p+1} kbp+1 ,并且右侧还能匹配剩余 m − ( p + 1 ) m-(p+1) m(p+1) ,即 suf [ i + 1 ] ≥ m − p − 1 \text{suf}[i+1]\ge m-p-1 suf[i+1]mp1

遍历所有 i ∈ [ 0 , n ] i\in[0,n] i[0,n],取能满足的最小 b p + 1 b_{p+1} bp+1 即为答案。如果前缀本身就能匹配 m m m 个,则输出 0,遍历后没有可行解则输出 − 1 -1 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 n n 的排列 p p p ,以及 q q q 次独立查询。每次查询给定区间 [ l , r ] [l,r] [l,r] 和目标值 x x x。在对这个区间上模拟二分查找时,如果当前中点值与 x x x 比较的结果和数组实际顺序不一致,就会失败。允许在查询前任意选取 d d d 个 不是 x x x 的下标,将它们对应的值重新任意重排,来保证查找成功。求每次查询的最小 d d d,若无法通过任何重排成功则输出 − 1 -1 1

解题思路

阅读理解题,理解题意后不难。

首先预处理出每个值在排列中的位置,对 i = 1 , 2 , … , n i=1,2,\dots ,n i=1,2,,n 记录 p o s [ i ] pos[i] pos[i]

对于每个查询 L , R , x L,R,x L,R,x,令 k = pos [ x ] k=\text{pos}[x] k=pos[x],若 k ∉ [ L , R ] k\notin[L,R] k/[L,R] 则直接输出 − 1 -1 1,否则在区间 [ l = L , r = R ] [l=L,r=R] [l=L,r=R] 上模拟二分查找,维护4个值:

c n t 0 cnt0 cnt0 :访问到且无需替换的 p [ m i d ] < x p[mid]<x p[mid]<x 次数

c n t 1 cnt1 cnt1 :访问到且无需替换的 p [ m i d ] > x p[mid]>x p[mid]>x 次数

c n t L cntL cntL :必须将 p [ m i d ] p[mid] p[mid] 当作 > x x x 来替换的次数

c n t R cntR cntR :必须当作 < x x x 来替换的次数

l = L , R = r , m i d = l + r 2 l=L,R=r,mid=\frac{l+r}{2} l=L,R=r,mid=2l+r

  • m i d < k mid \lt k mid<k p [ m i d ] < x p[mid]\lt x p[mid]<x 时,向右搜索,累加小于 x x x 的访问数 c n t 0 + 1 cnt0+1 cnt0+1
  • m i d < k mid<k mid<k p [ m i d ] > x p[mid]>x p[mid]>x 时,本来会向右但 p [ m i d ] p[mid] p[mid] 又不小于 x x x,因此必须把它当成大于 x x x 来替换, c n t L + 1 , c n t 1 + 1 cntL+1,cnt1+1 cntL+1,cnt1+1 ,并令 l = m i d + 1 l=mid+1 l=mid+1
  • m i d > k mid>k mid>k p [ m i d ] > x p[mid]>x p[mid]>x 时,向左搜索,累加大于 x x x 的访问数 c n t 1 + 1 cnt1+1 cnt1+1
  • m i d > k mid>k mid>k p [ m i d ] < x p[mid]<x p[mid]<x 时,本来会向左但 p [ m i d ] p[mid] p[mid] 又不大于 x x x,因此必须把它当成小于 x x x 来替换, c n t R + 1 , c n t 0 + 1 cntR+1,cnt0+1 cntR+1,cnt0+1 ,并令 r = m i d − 1 r=mid-1 r=mid1

m i d = k mid=k mid=k 停止。此时 c n t L cntL cntL 是需要替换成大于 x x x 的次数, c n t R cntR cntR 是需要替换成小于 x x x 的次数, c n t 0 cnt0 cnt0 c n t 1 cnt1 cnt1 分别是整个过程中访问到的小于/大于 x x x 的点数。可用的小于 x x x 的数目为 ( x − 1 ) − c n t 0 (x-1)-cnt0 (x1)cnt0,可用的大于 x x x 的数目为 ( n − x ) − c n t 1 (n-x)-cnt1 (nx)cnt1;若它们无法分别覆盖 max ⁡ ( 0 , c n t R − c n t L ) \max(0,cntR-cntL) max(0,cntRcntL) max ⁡ ( 0 , c n t L − c n t R ) \max(0,cntL-cntR) max(0,cntLcntR) 这两种多余替换需求,就输出 − 1 -1 1,否则最少替换次数为 2 max ⁡ ( c n t L , c n t R ) 2\max(cntL,cntR) 2max(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

题目大意

给定二值向量
s = ( s 1 , s 2 , … , s n ) ∈ { 0 , 1 } n , s=(s_1,s_2,\dots,s_n)\in\{0,1\}^n, s=(s1,s2,,sn){0,1}n,
构造二值矩阵 G ∈ { 0 , 1 } n × n G\in\{0,1\}^{n\times n} G{0,1}n×n
G i , j = { 1 − s i , i = j , s j , i ≠ j . G_{i,j}=\begin{cases} 1 - s_i,& i=j,\\ s_j,& i\neq j. \end{cases} Gi,j={1si,sj,i=j,i=j.
视矩阵中值为 0 的单元格以四连通(上下左右)方式连通,求这些连通分量中的最大的联通分量大小。

解题思路

对于每个下标 j j j 满足 s j = 0 s_j=0 sj=0,将该列分为三部分:

  • 上半段:所有 G i , j G_{i,j} Gi,j 使 1 ≤ i < j 1\le i\lt j 1i<j;共 j − 1 j-1 j1 个格子,记作节点 U j U_j Uj ,联通块大小为 j − 1 j-1 j1
  • 下半段:所有 G i , j G_{i,j} Gi,j 使 $ j$;共 n − j n-j nj 个格子,记作节点 D j D_j Dj ,联通块大小为 n − j n-j nj

对于每个下标 j j j 满足 s j = 1 s_j=1 sj=1 G j , j G_{j,j} Gj,j 为零,记作节点 T j T_j Tj,联通块大小为 1。

对于每个 1 ≤ j < n 1\le j\lt n 1j<n,若 s j = s j + 1 = 0 s_j=s_{j+1}=0 sj=sj+1=0,则:

  • 合并 U j U_j Uj U j + 1 U_{j+1} Uj+1(它们在行方向上上下段相连)
  • 合并 D j D_j Dj D j + 1 D_{j+1} Dj+1 (同理)

对每个 j j j 使 s j = 1 s_j=1 sj=1

  • j > 1 j>1 j>1 s j − 1 = 0 s_{j-1}=0 sj1=0,合并 T j T_j Tj D j − 1 D_{j-1} Dj1 G j , j G_{j,j} Gj,j 可向左下连通。
  • j < n j\lt n j<n s j + 1 = 0 s_{j+1}=0 sj+1=0,合并 T j T_j Tj U j + 1 U_{j+1} Uj+1 G j , j G_{j,j} Gj,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 n n 个节点。每个节点有一个初始权值,只有两种可能: + 1 +1 +1 − 1 -1 1

树有一个隐藏的根节点 rt \text{rt} rt,定义 f ( u ) = ∑ x ∈ rt ⇝ u val [ x ] f(u)=\sum_{x\in\text{rt}\rightsquigarrow u} \text{val}[x] f(u)=xrtuval[x] ,即为从根到节点 u u u 路径上所有节点权值之和。

你可以进行两种交互式操作,总次数不能超过 n + 200 n+200 n+200

  1. ? 1 k a_1 a_2 … a_k,会返回 f ( a 1 ) + f ( a 2 ) + … + f ( a k ) f(a_1)+f(a_2)+…+f(a_k) f(a1)+f(a2)++f(ak)
  2. ? 2 u,将节点 u u u 的权值从 + 1 +1 +1 − 1 -1 1 互换,为永久修改。

在交互结束后,你要输出所有节点最终的权值。

解题思路

询问? 1 1 1 得到 F 0 = f ( 1 ) . F_0=f(1). F0=f(1).

接着对每个 v = 2 , 3 , … , n v=2,3,\dots,n v=2,3,,n 分别询问 ? 1 1 v 得到 F 0 ( v ) = f ( v ) . F_0(v)=f(v). F0(v)=f(v).

由于树为星形,类似菊花图,如果 v ≠ r t v\neq\mathrm{rt} v=rt,必有 f ( v ) = f ( 1 ) + v a l ( v ) ⟹ v a l ( v ) = F 0 ( v ) − F 0 . f(v)=f(1)+\mathrm{val}(v) \Longrightarrow\mathrm{val}(v)=F_0(v)-F_0. f(v)=f(1)+val(v)val(v)=F0(v)F0. ,当 v = r t v=\mathrm{rt} v=rt 时上述等式不成立,需要后续操作加以区分。

发送? 2 1,将节点 1 的权值取反,再次发送? 1 1 1得到 F 1 = f ′ ( 1 ) . F_1=f'(1). F1=f(1).

设翻转前 v a l ( 1 ) = x \mathrm{val}(1)=x val(1)=x,隐藏根的权值为 v a l ( r t ) \mathrm{val}(\mathrm{rt}) val(rt),则 F 0 = v a l ( r t ) + x , F 1 = v a l ( r t ) − x ⟹ x = F 0 − F 1 2 F_0=\mathrm{val}(\mathrm{rt})+x, F_1=\mathrm{val}(\mathrm{rt})-x \Longrightarrow x=\frac{F_0-F_1}{2} F0=val(rt)+x,F1=val(rt)xx=2F0F1 ,此时节点 1 的最终权值为 − x -x x

如果 F 0 = x F_0 = x F0=x , 则隐藏根即为 1,此时直接令 v a l ( 1 ) = − x , v a l ( v ) = F 0 ( v ) − F 0 ( v ≥ 2 ) \mathrm{val}(1)=-x, \mathrm{val}(v)=F_0(v)-F_0(v\ge2) val(1)=x,val(v)=F0(v)F0(v2)

如果 F 0 ≠ x F_0 \neq x F0=x , 则隐藏根即不为 1,它在集合 2 , 3 , … , n {2,3,\dots,n} 2,3,,n 中。

假设对叶子进行翻转,那么路径和应该为 p r e ( v ) = F 0 ( v ) − 2 x . \mathrm{pre}(v)=F_0(v)-2x. pre(v)=F0(v)2x.

令候选集合 C = { x i } C=\{x_i\} C={xi},初始时 x i = i + 1 , 1 ≤ i ≤ n − 1 x_i=i+1,1\le i \le n-1 xi=i+1,1in1

进行二分,取一半候选集合为 S = { x i } , 1 ≤ i < ∣ C ∣ 2 S=\{x_i\} ,1\le i\lt \frac{|C|}{2} S={xi},1i<2C ,另一半为 $T={x_i},\frac{|C|}{2}\le i\le |C| $

询问? 1 |S| S,得到实测总和 r e a l real real,计算预测总和 e x = ∑ v ∈ S f ( v ) ex=\sum_{v\in S}f(v) ex=vSf(v)

如果 r e a l − e x = 2 x real-ex=2x realex=2x , 则根在集合 S S S,否则在 T T T。更新 C C C 为对应子集,直到 ∣ C ∣ = 1 |C|=1 C=1 。此时唯一元素即为隐藏根,再令 v a l ( r t ) = F 0 − x \mathrm{val}(\mathrm{rt})=F_0-x val(rt)=F0x , 连同先前存储的其他节点 v a l ( v ) \mathrm{val}(v) val(v) 一并输出。

最多查询次数 1 + ( n − 1 ) + 1 + 1 + ⌈ log ⁡ 2 ( n − 1 ) ⌉ ≤ n + 12 < n + 200 1+(n-1)+1+1+\lceil\log_2(n-1)\rceil \le n+12 < n+200 1+(n1)+1+1+log2(n1)⌉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 u u 的“邻居二分”原理。设 p u p_u pu 为父亲,其余相邻节点记作 c 1 , c 2 , … , c k c_1,c_2,\dots,c_k c1,c2,,ck。翻转 u u u 后,父亲的路径和保持 s p u s_{p_u} spu 不变,而每个非父亲邻居的路径和统一变化 ± 2 \pm2 ±2,于是有

s c i ′ − s c i = ± 2 ( i ≠ p u ) , s p u ′ − s p u = 0. s_{c_i}'-s_{c_i}= \pm2 \quad(i\ne p_u),\quad s_{p_u}'-s_{p_u}=0. scisci=±2(i=pu),spuspu=0.

若一次查询某个邻居子集 Σ \Sigma Σ 的路径和并记为 S 1 S_1 S1,翻转 u u u 再查询同一子集得到 S 2 S_2 S2,则差值满足

∣ S 1 − S 2 ∣ = { 2 ∣ Σ ∣ , p u ∉ Σ , < 2 ∣ Σ ∣ , p u ∈ Σ . |S_1-S_2|= \begin{cases} 2|\Sigma|,& p_u\notin \Sigma,\\ <2|\Sigma|,& p_u\in \Sigma. \end{cases} S1S2={2∣Σ∣,<2∣Σ∣,pu/Σ,puΣ.

因此用二分法:把邻居按任意顺序分成左右两段,三次查询(求和、翻转、再求和)即可判断父亲在哪一侧,重复 ⌈ log ⁡ 2 k ⌉ \lceil\log_2 k\rceil log2k 轮后得到唯一父亲;若过程中发现邻居集合为空,则 u u u 本身即为根。一次找父亲总查询次数为 3 log ⁡ k 3\log k 3logk

要保证总查询量不超标,需要在小而优的节点上调用“找父亲”。树的重心 c c c 具有删除后所有连通块大小 ≤ ∣ S ∣ 2 \le\frac{|S|}{2} 2S 的性质,其中 ∣ S ∣ |S| S 是当前候选根集合大小。取当前候选集合的重心 c c c 并对 c c c 执行上述二分:
若找不到父亲,则 c c c 就是真根;若找到父亲 p c p_c pc,真实根必位于包含 p c p_c pc 的那一侧,只需一次 DFS 将另一侧整块节点标记为非候选。候选集合大小至少减半,所以重心过程至多进行 ⌈ log ⁡ 2 n ⌉ \lceil\log_2 n\rceil log2n 轮;每轮用到的查询数上界为 3 log ⁡ ( deg ⁡ c ) 3\log(\deg c) 3log(degc),当 n ≤ 1000 n\le1000 n1000 时总计严格小于 165 165 165

r t rt rt 确定后,再对每个结点单点查询一次? 1 1 u即可得到所有路径和 s u s_u su,共用 n n n 次查询。随后 DFS 构建父子关系并利用 v r = s r , v u = s u − s p u ( u ≠ r ) v_r=s_r,\quad v_u=s_u-s_{p_u}\quad(u\ne r) vr=sr,vu=suspu(u=r) ,就能还原整棵树的最终取值,它们必然落在 + 1 , − 1 {+1,-1} +1,1,因为对真实父子边 ( p , u ) (p,u) (p,u) s u − s p u = v u ∈ { ± 1 } s_u-s_{p_u}=v_u\in\{\pm1\} suspu=vu{±1}

总的查询复杂度为 n + 165 < n + 200 n+165<n+200 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,模拟玩了一下,想到可以集合二分判根,写了一下就过了。挺有意思的交互题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值