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

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 o o t v\neq\mathrm{root} v=root,必有 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######################################*/
// 链接:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值