2023.03.26 CodeforcesRound 860 (Div2) A~E

A. Showstopper

【题意】

给两个长度为 n n n 的序列 a , b a,b a,b,每次操作可以选择一个位置 i i i 交换 a i , b i a_i,b_i ai,bi。问能否通过任意次操作使得 a n , b n a_n,b_n an,bn 分别是 a , b a,b a,b 中最大的元素。

【分析】

const int N = 5e5 + 7;
int a[N], b[N];
int n;

void SolveTest() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
    }
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &b[i]);
    }

    if (a[n] < b[n]) {
        swap(a[n], b[n]);
    }

    for (int i = 1; i < n; i++) {
        if (max(a[i], b[i]) > a[n] or min(a[i], b[i]) > b[n]) {
            puts("No");
            return;
        }
    }
    puts("Yes");
}

B. Three Sevens

【题意】

m m m 个集合。需要从每个集合中找到一个元素,满足它在之后的集合中再也没有出现过,或报告不可能。

【分析】

int m, n;
set<int> st[50005];
int ans[50005];

void SolveTest() {
    cin >> m;
    for (int i = 1; i <= m; i++) {
        st[i].clear();
        cin >> n;
        for (int j = 1; j <= n; j++) {
            int x;
            cin >> x;
            st[i].insert(x);
        }   
    }

    set<int> tot;

    for (int i = m; i >= 1; i--) {
        bool flg = 0;
        for (int x : st[i]) {
            if (tot.find(x) == tot.end()) {
                flg = 1;
                ans[i] = x;
            }
            tot.insert(x);
        }
        if (!flg) {
            puts("-1");
            return;
        }
    }

    for (int i = 1; i <= m; i++) {
        printf("%lld ", ans[i]);
    }
    puts("");
}

C. Candy Store

【题意】

n n n 种糖果,每种糖果有个数 a [ i ] a[i] a[i],单价 b [ i ] b[i] b[i]。现在需要将每一种糖果,分成若干数量相同的组,第 i i i 种糖果的每一组有 d [ i ] d[i] d[i] 个。第 i i i 种糖果的价格标签为 c [ i ] = b [ i ] × d [ i ] c[i] = b[i]\times d[i] c[i]=b[i]×d[i]。如果两个相邻的糖果价格标签相同,则他们可以共用一个价格标签。

现在给出所有 a a a b b b ,需要设计每种糖果的 d d d,使得总的价格标签数量最少。

题目限定糖果顺序不能打乱。

【分析】

连续若干个糖果 [ l , r ] [l,r] [l,r] 可以合并为一个价格标签 x x x

首先每一组的数量 d [ i ] = x b [ i ] d[i] = \dfrac{x}{b[i]} d[i]=b[i]x 都是整数,所以对于 i ∈ [ l , r ] i\in[l,r] i[l,r],都有 b [ i ] ∣ x b[i]\mid x b[i]x,即
lcm ⁡ ( b l , . . . b r ) ∣ x \operatorname{lcm}(b_l,...b_r) \mid x lcm(bl,...br)x
其次组的个数一定是整数,即 d [ i ] ∣ a [ i ] d[i] \mid a[i] d[i]a[i]。这个不好直接处理,但是注意到组的个数其实就是 a [ i ] × b [ i ] x \dfrac{a[i]\times b[i]}{x} xa[i]×b[i],即该种物品的总价除以每一组的总价。令 t [ i ] = a [ i ] × b [ i ] t[i] = a[i]\times b[i] t[i]=a[i]×b[i],则对于 i ∈ [ l , r ] i\in [l,r] i[l,r],都有 x ∣ t [ i ] x\mid t[i] xt[i],即
x ∣ gcd ⁡ ( t l , . . . , t r ) x\mid \gcd(t_l, ...,t_r) xgcd(tl,...,tr)
综上可以得到
lcm ⁡ ( b l , . . , b r ) ∣ gcd ⁡ ( t l , . . . , t r ) \operatorname{lcm}(b_l,..,b_r) \mid \gcd(t_l, ...,t_r) lcm(bl,..,br)gcd(tl,...,tr)
从左到右扫描即可。

const int N = 5e5 + 7;
int n;
int a[N], b[N], t[N];

int gcd(int x, int y) {
    return __gcd(x, y);
}

int lcm(int x, int y) {
    return x / gcd(x, y) * y;
}

void SolveTest() {
    cin >> n;
    int res = 0, g = 0, l = 1;
    for (int i = 1; i <= n; i++) {
        scanf("%lld%lld", &a[i], &b[i]);
        t[i] = a[i] * b[i];
        g = gcd(g, t[i]);
        l = lcm(l, b[i]);

        if (i == 1 or g % l) {
            res++;
            g = t[i];
            l = b[i];
        }
    }

    printf("%lld\n", res);
}

D. Shocking Arrangement

【题意】

给定序列 a a a,保证 a a a 中的元素和为 0 0 0。需要将 a a a 重新排序,满足
max ⁡ 1 ≤ l ≤ r ≤ n ∣ ∑ i = l r a i ∣ < max ⁡ i = 1 n a i − min ⁡ i = 1 n a i \max_{1\le l\le r\le n}|\sum_{i=l}^{r}a_i|<\max_{i=1}^{n}a_i - \min_{i=1}^{n}a_i 1lrnmaxi=lrai<i=1maxnaii=1minnai
也就是说:任何一个区间的和的绝对值都小于序列最大值与最小值的差。

输出一组可行的排序,或者输出不可能。

【分析】

比较直观的想法是要尽可能地把正数和负数交替开,这样可以避免出现一段连续区间都是同号,加起来绝对值过大的情况。

比赛的时候我尝试过一种错误方法,以失败告终:把正数和负数分别放到两个序列中,并排序。正数从大到小,负数从小到大。对于答案序列 a n s ans ans,把连续的一段符号相同的区间成为一个段。贪心地让这个段的和的绝对值小于 max ⁡ i = 1 n a i − min ⁡ i = 1 n a i \max_{i=1}^{n}a_i - \min_{i=1}^{n}a_i maxi=1naimini=1nai (定值)。如果继续添加同号元素会让这个段的和绝对值超标,那么就转为添加另一个符号的元素,即开始一个新的段。

这个做法失败的原因在于,这种贪心方法虽然可以保证当前段的和绝对值不超标,但是可能导致更多另一种符号的元素堆积在后面。例如 3 , 3 , − 1 , − 1 , − 1 , − 1 3,3,-1,-1,-1,-1 3,3,1,1,1,1。一个想法是,既然该例中负数更多,那么就在答案中先从负数开始加。但是贪心的时候无法保证正负数个数的平衡,无法杜绝分段个数增加之后某种符号的元素在末尾堆积。

下面开始介绍正解:

一个关键的点是利用起来元素和为 0 0 0 这个条件。这个条件能推出的结论是:序列无解的唯一情况是 a a a 中所有元素均为 0 0 0,否则一定能构造出来合法解。

合法解的构造方法如下。

  1. 记构造的答案为 a n s ans ans。首先把所有的 0 0 0 放到 a n s ans ans 的最前面。这样 a a a 中剩余的元素都不为 0 0 0
  2. a n s ans ans 已有的元素和为 s u m sum sum。重复以下操作直到 a a a 为空。
    • 如果 s u m ≤ 0 sum\le 0 sum0:从 a a a 中剩余的元素中任选一个正数加入 a n s ans ans 末尾,并把它从 a a a 中删除;
    • 如果 s u m > 0 sum>0 sum>0:从 a a a 中剩余的元素中任选一个负数加入 a n s ans ans 末尾,并把它从 a a a 中删除。

下面证明这个构造方法的正确性。

a n s ans ans 的前缀和为 P [ i ] P[i] P[i]。则可转化所求为
max ⁡ 1 ≤ l ≤ r ≤ n ∣ ∑ i = l r a n s i ∣ = max ⁡ i = 1 n P ( i ) − min ⁡ i = 1 n P ( i ) \max_{1\le l\le r\le n}|\sum_{i=l}^{r}ans_i|=\max_{i=1}^{n}P(i) - \min_{i = 1}^{n}P(i) 1lrnmaxi=lransi=i=1maxnP(i)i=1minnP(i)
max ⁡ i = 1 n P ( i ) \max_{i=1}^{n}P(i) maxi=1nP(i) 一定是在 i i i 为正数的时候取到,记下标为 u u u。由于在添加正数 a n s [ u ] ans[u] ans[u] 的时候保证了 P [ u − 1 ] ≤ 0 P[u-1] \le 0 P[u1]0 ,所以有 P [ u ] = P [ u − 1 ] + a n s [ u ] ≤ a n s [ u ] P[u]=P[u - 1] + ans[u] \le ans[u] P[u]=P[u1]+ans[u]ans[u]

min ⁡ i = 1 n P ( i ) \min_{i=1}^{n}P(i) mini=1nP(i) 一定是在 i i i 为负数的时候取到,记下标为 v v v。由于在添加负数 a n s [ v ] ans[v] ans[v] 的时候保证了 P [ v − 1 ] > 0 P[v-1] > 0 P[v1]>0 ,所以有 P [ v ] = P [ v − 1 ] + a n s [ v ] > a n s [ v ] P[v]=P[v - 1] + ans[v] > ans[v] P[v]=P[v1]+ans[v]>ans[v]

因此可以得到
max ⁡ 1 ≤ l ≤ r ≤ n ∣ ∑ i = l r a n s i ∣ = max ⁡ i = 1 n P ( i ) − min ⁡ i = 1 n P ( i ) ≥ P ( u ) − P ( v ) > a n s u − a n s v = max ⁡ i = 1 n a n s i − min ⁡ i = 1 n a n s i \begin{aligned} \max_{1\le l\le r\le n}|\sum_{i=l}^{r}ans_i| &= \max_{i=1}^{n}P(i) - \min_{i = 1}^{n}P(i)\\ &\ge P(u)-P(v)\\ &> ans_u-ans_v \\ &= \max_{i=1}^{n}ans_i - \min_{i = 1}^{n}ans_i \end{aligned} 1lrnmaxi=lransi=i=1maxnP(i)i=1minnP(i)P(u)P(v)>ansuansv=i=1maxnansii=1minnansi

int n;

void SolveTest() {
    int cnt0 = 0, sum = 0, x;
    queue<int> q1, q2;
    vector<int> ans;

    cin >> n;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &x);
        if (x > 0) {
            q1.push(x);
        } else if (x < 0) {
            q2.push(x);
        } else {
            cnt0++;
        }
    }

    if (cnt0 == n) {
        puts("No");
        return;
    }

    while (cnt0) {
        ans.push_back(0);
        cnt0--;
    }

    while (q1.size() or q2.size()) {
        if (sum <= 0 and q1.size()) {
            int x = q1.front();
            sum += x;
            ans.push_back(x);
            q1.pop();
        } else {
            int x = q2.front();
            sum += x;
            ans.push_back(x);
            q2.pop();
        }
    }

    puts("Yes");
    for (int x : ans) {
        printf("%lld ", x);
    }
    puts("");
}

E. Multitest Generator

【题意】

定义一个数组 b [ 1 , m ] b[1,m] b[1,m] 是一个 test:满足 b 1 = m − 1 b_1=m-1 b1=m1

定义一个序列 b [ 1 , m ] b[1,m] b[1,m] 是一个 multitest:满足 b [ 2 , m ] b_[2,m] b[2,m] 可以被划分为 b 1 b_1 b1 个子序列,每一个子序列都是一个 test。

定义函数 f ( b [ 1 , m ] ) f(b[1,m]) f(b[1,m]):对于序列 b [ 1 , m ] b[1,m] b[1,m],每次操作可以将一个元素替换为任意数字,至少要多少次操作可以使它变为一个 multitest。

给定序列 a a a,对于所有的 i ∈ [ 1 , n − 1 ] i\in[1,n-1] i[1,n1],求 f ( a [ i , n ] ) f(a[i,n]) f(a[i,n])

n , a [ i ] ∈ 300000 n,a[i]\in 300000 n,a[i]300000

【分析】

对于任何一个长度为 n n n 的序列,一定可以通过不超过两次操作使得它是一个 multitest。即让第一个数字为 1 1 1,第二个数字为 n − 2 n-2 n2

首先讨论什么情况下需要 0 0 0 次操作。

如果 a [ i , n ] a[i,n] a[i,n] 是由若干个连续的 test 组成,那么定义下标 i i i 是 ”好下标”。

对于每一个下标,我们想要知道它是否为 “好下标”。如果是的话,这个后缀由多少个 tests 组成。

定义 n x t [ i ] = i + a i + 1 nxt[i] = i + a_i + 1 nxt[i]=i+ai+1。这个定义的想法是如果 i i i 是一个 test 的首元素,那么 i + a i + 1 i+a_i +1 i+ai+1 就是下一个 test 的首元素。

那么下标 i i i 是 “好下标” 等价于:链 i → n x t [ i ] → n x t [ n x t [ i ] ] → . . . i \rightarrow nxt[i] \rightarrow nxt[nxt[i]] \rightarrow ... inxt[i]nxt[nxt[i]]... 最终会在 n + 1 n+1 n+1 结束。

可以简单地用 DP 处理每个这个链是否会在 n + 1 n+1 n+1 结束,以及这条链有多少个 test,即链的深度 d e p [ i ] dep[i] dep[i]

nxt[i] = i + a[i] + 1;

if (nxt[i] == n + 1 || (nxt[i] <= n && good[nxt[i]])) {
    good[i] = 1;
} else {
    good[i] = 0;
}

dep[i] = 1 + dep[min(nxt[i], n + 1)];

综上,为了确定后缀 a [ i , n ] a[i,n] a[i,n] 是否为一个 multitest,我们需要判断 i + 1 i+1 i+1 是否为 “好下标”,并查询后缀 a [ i + 1 , n ] a[i+1, n] a[i+1,n] 是否包含 a i a_i ai 个test。

然后讨论什么情况下需要 1 1 1 次操作。

如果需要一次操作的话,要么是第一个元素(表示有多少个 test)改变了,要么是某个 test 当中的元素改变了。

第一种情况,第一个元素改变了。当且仅当 i + 1 i+1 i+1 是 “好下标”。

第二种情况,某个 test 当中的某个元素改变了。

改变后的下标将会是某个 test 的首元素,即 i + 1 , n x t [ i + 1 ] , n x t [ n x t [ i + 1 ] ] . . . i+1,nxt[i+1],nxt[nxt[i+1]]... i+1,nxt[i+1],nxt[nxt[i+1]]... 之一。否则这条链会保持原状,什么也没有改变。那么这个改变后的下标表示了。

我们考虑后缀 a [ i , n ] a[i,n] a[i,n] 通过改变某一个元素,最多能使这个后缀包含多少个连续的 test,记为 d [ i ] d[i] d[i]。为什么要考虑最多呢?因为如果能得到 d [ i ] d[i] d[i] 个 test,那么对于所有 c n t < d [ i ] cnt<d[i] cnt<d[i],都一定能得到 c n t cnt cnt 个 test (只需要调整改变的那个元素的大小,让若干个 test 合并为一个 test 即可)。

所以如果 d [ i + 1 ] ≥ a i d[i+1] \ge a_i d[i+1]ai,那么后缀 a [ i , n ] a[i,n] a[i,n] 可以通过一次操作变为 multitest。

下面考虑怎样维护 d d d 的值。从后向前遍历的时候,维护 m a x d e p [ i ] maxdep[i] maxdep[i],即:从 i i i 以及它之后出发的链最大深度。

if (good[i]) {
    maxdep[i] = max(maxdep[i + 1], dep[i]);
} else {
    maxdep[i] = maxdep[i + 1];
}

那么我们可以用一次操作修改 a [ i ] a[i] a[i],使得以 a [ i ] a[i] a[i] 打头的 test 能够接上它之后深度最大的那一条链。

d[i] = 1 + maxdep[i + 1];

完整代码如下:

const int N = 5e5 + 7;

int n;
bool good[N];
int a[N], nxt[N], dep[N], maxdep[N], d[N], ans[N];

void SolveTest() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
    }

    dep[n + 1] = 0;
    maxdep[n + 1] = 0;
    d[n + 1] = 0;
    
    for (int i = n; i >= 1; i--) {
        nxt[i] = i + a[i] + 1;

        if (nxt[i] == n + 1 || (nxt[i] <= n and good[nxt[i]])) {
            good[i] = 1;
        } else {
            good[i] = 0;
        }

        dep[i] = 1 + dep[min(nxt[i], n + 1)];
        d[i] = 1 + maxdep[i + 1];

        if (nxt[i] <= n + 1) {
            d[i] = max(d[i], 1 + d[nxt[i]]);
        }

        if (good[i]) {
            maxdep[i] = max(maxdep[i + 1], dep[i]);
        } else {
            maxdep[i] = maxdep[i + 1];
        }
    }

    for (int i = 1; i < n; i++) {
        if (good[i + 1] && dep[i + 1] == a[i]) {
            ans[i] = 0;
        } else if (good[i + 1] || d[i + 1] >= a[i]) {
            ans[i] = 1;
        } else {
            ans[i] = 2;
        }
    }

    for (int i = 1; i < n; i++) {
        printf("%lld ", ans[i]);
    }
    puts("");
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值