Codeforces Round #673 (Div. 2)[A-E]

Codeforces Round #673 (Div. 2)[A-E]

A. Copy-paste

题目大意

给定一个长为 \(n\) 的数组 \(a\) ,你可以对任意 $ i, j ;(i\neq j)$ ,进行操作 \(a_j=a_i+a_j\)。同时,需要满足数组中任意一项值不超过 \(k\),求最多可进行多少次操作。

*800 greedy

思路分析

这题,显然是一个离线贪心。不了解的可以查看我的贪心算法专题、

排序之后,把最小值不断赋值给其他数直到无法继续赋值。

对于这种题,毫无疑问贪心算法或者数学法会是更好的选择。

代码

const int maxn = 1e5 + 50;
int f[maxn], mn, pos;
void solve(){
    int n = read(), k = read();
    for (int i = 0; i < n; ++ i) f[i] = read();

    sort(f, f + n);
    LL sum(0);
    for (int i = 1; i < n; ++ i) sum += (k - f[i]) / f[0];
    cout << sum << '\n';
}

B. Two Arrays

题目大意

给定一个长为 \(n\) 的非负整数序列 \(a\) ,和一个定值 \(T\)

定义 \(f(b)\) 为:

序列 \(b\) 中,满足 \(b_i + b_j = T\) 的点对 \((i, j) \; i < j\) 的总数。

现在,要求对序列 \(a\) 进行黑白染色,使其分为两个子序列 \(c, d\) 。求问使得 \(f(c) + f(d)\) 最小的染色方案。

*1100

思路分析

本题具有两个思路,分别为 基于数学推演的情况讨论法基于贪心算法的暴力比较法

假如在竞赛过程中,如果能大致保证贪心算法的正确性时,用贪心算法显然更好一些。

这里主要介绍贪心算法,数学推演实际上是将小于 \(\dfrac{T}{2}\) 的分为一组, 大于 \(\dfrac{T}{2}\) 的分为一组。等于 \(\dfrac{T}{2}\) 的在两子序列中周期摆动安放。

而贪心算法则是维护两个子序列的 hashmap 。当要插入一个数字时,比较其在两个子序列中分别能组成多少组合法点对,贪心选择数量较少的进行插入,满足要求。

在看到题面的时候,通过注意到 \(b_i + b_j = T\) ,我们应该快速联想到 hashmap 解决此类问题。

在看到最小时,应该快速思考使用在线 or 离线贪心算法。

代码

const int maxn = 1e5 + 50;
int f[maxn], ans[maxn];
void solve(){
    int n = read(), T = read();
    for (int i = 0; i < n; ++ i) f[i] = read();

    unordered_map<int, int> mp1, mp2;
    mp1.clear(), mp2.clear();

    VI ans(n);
    for (int i = 0; i < n; ++ i){
        if (mp1[T - f[i]] <= mp2[T - f[i]]){
            ans[i] = 0;
            ++ mp1[f[i]];
        }else {
            ans[i] = 1;
            ++ mp2[f[i]];
        }
    }
    show(ans);
}

C. k-Amazing Numbers

题目大意

给定一个长为 \(n\) 的序列 \(a\),该序列元素大小均在 \([1, n]\) 中。定义 k惊喜数字

  • 在序列 \(a\) 所有长为 \(k\) 的连续子序列中均出现过的最小元素,若不存在则赋值为 \(-1\)

请你计算并输出 \(k \in [1, n]\) 的所有 k惊喜数字

1 <= n <= 3e5

*1500

思路分析

刚拿到这题时,我没有一点思路。一直在思考是不是滑动窗口,以及如何复用其中的信息,均发现无法快速解决。

后面转换角度从每个元素的特性出发,插入头尾两个虚拟结点,寻找各元素之间的最大间隔。并发现这个方法可行。

最终得出如下结论:

  • 假如你不自觉的想入一个问题且在一定时间内不能得出答案时,说明你有一个细节或者角度不对。
  • 如果不自觉的用宏观解决问题时(比如在段,区间),专注于微观可能是这个题目的解决方案(位,各元素之间)

之所以写上面这段话,是因为这题当时险些没有做出,而且不属于方法不清楚的层面,而是我在思考问题时出现了角度的偏差。谨以此警示。

回归这个题目,如上文所提,我们维护每个元素的最大间隔。易知,当 \(k\) 大于某元素最大间隔时,就可能对k惊喜数字产生贡献。在利用 std::map 维护每元组的间隔时,由于其自动排序的特性,可以从头至尾进行处理。

代码

const int maxn = 3e5 + 50;
int f[maxn];
void solve(){
    int n = read(), tot = 0, cnt(0);
    map<int, int> dis; // 最大间距
    unordered_map<int, int> pre; // 上一个元素出现位置
    for (int i = 1; i <= n; ++ i){ // index from 1 (in order to insert 0-index node)
        f[i] = read();
        if (pre[f[i]] == 0){
            pre[f[i]] = i;
            dis[f[i]] = i;
        }else {
            int dlt = i - pre[f[i]]; // 间距
            // wprint(dlt, dis[f[i]]);
            pre[f[i]] = i;
            if (dlt > dis[f[i]]) dis[f[i]] = dlt;
        }
    }
    for (auto &e: pre){
        int dlt = n + 1 - e.second; // insert (n+1)-index node 
        if (dlt > dis[e.first]) dis[e.first] = dlt;
        // wprint(dlt, e.first, e.second);
    }

    vector<int> ans(n + 1, -1);
    int stop = n + 1;
    for (auto &e: dis){ // 利用 map 的排序特性,从头至尾处理即可
        if (e.second >= stop) continue;
        for (int i = e.second; i < stop; ++ i) ans[i] = e.first;
        stop = e.second;
    }
    for (int i = 1; i <= n; ++ i) cout << ans[i] << (i == n ? '\n' : ' ');
}

D. Make Them Equal

题目大意

  • 出一个序列 \(a\),求出一个长度不超过 \(3n\) 的操作序列,每次操作之后序列中所有元素必须为非负整数,操作完成后使序列 \(a\) 中每个元素相等。
  • 定义一次操作为:选出 \((i,j,x)\) 三元组,满足 \(i,j\) 为序列合法下标,\(x\)\(10^9\) 以内非负整数,令 \(a_i:= a_i-x\cdot i,a_j:=a_j+x\cdot i\)
  • 输出时先输出操作次数 \(k\),然后输出 \(k\) 行操作序列。

*2000

思路分析

*2000 分的构造题,实际上比完赛之后想想还是很套路的。以后有机会总结一个 你只能用最多 \(c \cdot n\) 次操作,完成一个目标

在这里进行一个简单的总结。

用 c * n 次操作(询问)完成目标

首先,不管可用操作(询问)数为多少,一个永远可以尝试的思路是: 以某个最特殊的元组做跳板

  • \(c == 1\),则为线性操作类似冒泡的感觉,从头至尾线性操作,每次保留其中之一在下一次进行操作。
  • \(c == 2\),一般来说:
    • 可能为 \(c == 1\)时的情况,但是每次操作需要两步C. Chocolate Bunny 类似这题。
    • 或者对每个元素用一次操作将其转移到跳板上,再进行一次操作完成目的。
  • \(c==3\),则想法与上述差不多。

总而言之,需要发现操作的特定性质,如互相操作跳板操作

对于这题,比较困难的地方在于每次操作之后保证所有的元素非负。根据上述结论显然我们需要选择一共“跳板

”,毫无疑问这个跳板选择初始位置 \(1\) 最为合适。因此,我们大致制定好了构造策略:

  1. 首先将其他位置的元素转移到“跳板上”。
  2. 在借助跳板分配元素,使得每个元素相等。

根据题设限制,每个元素最多转移 \(a_i - (a_i) \%i\) ,对于余下的 \(a_i \% i\) ,我们可以先补充上 \(i - (a_i \% i)\),再进行转移。经过思考,这个方案是满足条件的,实际上他也符合一种贪心的思想。我们尽可能避免对于元素进行较大的减操作

因此最终策略为:

  • 首先,将元素转移到跳板上:
    • 先将元素补齐至 \((a_i + \Delta) \% == 0\)
    • 将元素转移到跳板上
  • 通过跳板逐步分配元素。

代码

const int maxn = 1e4 + 50;
int f[maxn];
void solve(){
    int tot(0), n = read();
    for (int i = 1; i <= n; ++ i) f[i] = read(), tot += f[i];

    if (tot % n != 0){ cout << "-1\n"; return; } // 不能整除显然不行

    int eve = tot / n;
    vector< tuple<int, int, int> > res;
    for (int i = 2; i <= n; ++ i){
        if (f[i] % i == 0) res.pb({i, 1, f[i] / i});
        else {
            res.pb({1, i, i - (f[i] % i)});
            res.pb({i, 1, (f[i] / i) + 1});
        }
    }
    for (int i = 2; i <= n; ++ i) res.pb({1, i, eve});
    
    wprint(sz(res));
    for (auto &e: res) wprint(get<0>(e), get<1>(e), get<2>(e));
}

E. XOR Inverse

题目大意

给定长度为 \(n\) \((1\le n\le3\times 10^5)\) 的数列 \(\{a_n\}(0\le a_n\le 10^9)\),请求出最小的整数 \(x\) 使 \(\{a_n\oplus x\}\)逆序对数最少,其中$ \oplus$ 是异或。

*2000 divide and conquer CDQ分治

思路分析

本题最开始会比较容易想到通过 dp 去解决。考虑到逆序对这个经典的问题,便考虑到分治的思路。实际上 bit 的 0 or 1 可以类比为二叉树的左右孩子。若越靠近根结点代表的 bit 越高,则右孩子一定大于左孩子(用了字典树的知识)。

因此,题目便比较简单了:运用 CDQ 分治的思路:

首先,我们维护一个 dp 数组,dp[bit][i]:= x 的第 bit 位为 i 时逆序对的个数

因为,两个元素比较大小等价于比较两者第一个不同位。不同位为 1 的更大。因此在解题的过程中,维护两个数组,左边代表当前分治到左边的元素(bit is 0),右边代表当前分治到右边的元素(bit is 1) 。因此,右边一定大于左边,在利用求解逆序对的知识,通过双指针处理两个子树合并出现的逆序对个数。

  • 若不修改该 bit (等价于 x 的第 bit 位为 0 ),则逆序对为直接求得的。
  • 若修改该bit (等价于 x 的第 bit 位为 1 ),则逆序对数为总数-不修改时的逆序对个数

之后,贪心选取其中每 bit 逆序对较小的构造答案。

代码

#define pb push_back
using VI = vector<int>;
using LL = long long;

// CDQ 分治处理点对问题
int dp[35][2];
void helper(VI cur, int bit = 30){
    if (bit < 0 or cur.empty()) return;

    int cnt1(0), ans1(0);
    int cnt2(0), ans2(0);
    VI right, left;

    for (auto &x: cur){
        if ((x >> bit) & 1){
            ans1 += cnt2; // 由于 index 从大到小,因此 cnt2 代表 index 大于当前元素且元素值小于当前元素的点对数。
            ++ cnt1;
            right.pb(x);
        }else {
            ans2 += cnt1;
            ++ cnt2;
            left.pb(x);
        }
    }
    helper(left, bit - 1), helper(right, bit - 1);
    dp[bit][0] += ans1;
    dp[bit][1] += ans2;
} 

void solve(){
    int n = read();
    VI a(n);
    for (auto &&e: a) e = read();
    reverse(all(a));
    helper(a);
    LL ans(0), res(0);
    for (int i = 0; i <= 30; ++ i){
        ans += min(dp[i][0], dp[i][1]);
        if (dp[i][1] < dp[i][0]) res |= (1 << i);
    }
    cout << ans << ' ' << res << '\n';
}	
posted @ 2020-10-01 09:13  Last_Whisper  阅读(237)  评论(0编辑  收藏  举报