D.翻转数列

题目链接:https://ac.nowcoder.com/acm/contest/11176/D

题意:

给定一个长度为 n 的数列 a,每次可以选择其中一段连续的区间,将其翻转,问在不超过 k 次翻转的前提下最多可以得到多少(数对中的两个数在数列中相邻且不同)的数对。(\(1 \le n \le{5\times{10^5}},0 \le k \le N,1\le a_i \le N\))

思路:

首先考虑我们的每一次翻转:

  1. 对于其中的数不同的两个[数对中的两个数在数列中相邻且相同的数对],我们进行一次翻转可以将这两个数对变为两个[数对中的两个数在数列中相邻且不同的数对],如 11...22,这样我们一次翻转可以增加两个[数对中的两个数在数列中相邻且不同的数对],我们对其进行翻转可以得到 12...12,这也是我们一次翻转能够得到的最佳结果

  2. 若当前只剩一种[数对中的两个数在数列中相邻且相同的数对],我们一次翻转最多只能使得[数对中的两个数在数列中相邻且不同的数对]增加一个,如 12...44...44,想要使[数对中的两个数在数列中相邻且不同的数对]数目增加,我们一次只能选择两个 44 数对中的一个进行翻转,即翻转成如 14...24...44

另外,若数列中出现次数最多的那个数的出现次数 \(maxnum\) 如果大于\(\lceil{n}\rceil\)(即计算机整数除法下的\(\frac{n+1}{2}\)),则答案最大不超过 \((n-maxnum)*2\) (即除了出现次数最多的那个数之外的数只有 \(n-maxnum < \frac{n}{2}\) 个,因此每个数出现次数最多的那个数组成两个相邻且不同的数对的情况下最多也只有 \((n-maxnum)*2\) 个)

综上,我们本题的思路即为:

  1. 输入数对中的所有数,并统计出现次数最多的那个数的出现次数 \(maxnum\)

  2. 数对中的数的值为索引来统计[数对中的两个数在数列中相邻且相同]的数对的个数(即分别统计[数对中的数不同相邻且相同的数对]各有多少个)

  3. 将这些数对按个数排好序(使用 multiset 很方便)

  4. 每次将个数最多的那种数对与任意一种其他的数对的数量同时减 1答案加 2翻转次数减 1,直到翻转次数为 0已经不存在两种以上的这样的数对

  5. 经过步骤 3 后翻转次数不为 0还剩下的[数对中的两个数在数列中相邻且相同的数对]的个数不为 0,则说明还剩下一种这样的数对,每次翻转可以将一种这样的数对变为相邻且不同的数对,即还可增加 \(min(剩余翻转次数,剩余相邻且相同的数对个数)\)相邻且不同的数对。

  6. 最后再对答案 \(ans\)\(min(ans,(n-maxnum)*2)\)即可得到最终答案

代码:

#include <iostream>
#include <set>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
    int n, k;
    cin >> n >> k; //数的总个数n,最多可以翻转k次
    vector<int> a(n), cnt(n, 0);
    for (int i = 0; i < n; ++i) //输入n个数
    {
        cin >> a[i];
        --a[i];      //输入的每个数a[i]满足1<=a[i]<=n,将其范围映射为0~n-1
        ++cnt[a[i]]; //统计每个数的个数
    }
    sort(cnt.begin(), cnt.end());
    int maxnum = *(cnt.end() - 1); //记录数量最多的那个数的个数

    cnt = vector<int>(n, 0); //n个元素,初值为0 //空间重用
    int ans = 0;
    for (int i = 1; i < n; ++i) //遍历每两个相邻的数形成的数对
        if (a[i - 1] == a[i])   //按数对中的数的值来统计每种两个数相邻且相同的数对个数
            ++cnt[a[i]];
        else //若不同则答案+1
            ++ans;
    multiset<int> st(cnt.begin(), cnt.end());
    //将两个数相邻且相同的数对,将其一一放入multiset st中按出现次数排好序

    st.erase(0);                   //将出现次数为0的数对全部删除
    while (st.size() > 1 && k > 0) //只要还有翻转次数(k不为0)且还存在(其中的数不同的)(两个数相邻且相同的数对)(multiset元素个数大于1)
    {
        int x = *st.begin();      //取数量最少的数对个数
        st.erase(st.begin());     //删除
        int y = *prev(st.end());  //取数量最多的数对个数
        st.erase(prev(st.end())); //删除
        --x, --y, --k, ans += 2;  //两种数对的个数都减1,翻转次数减1,答案加2
        if (x)                    //若此种数对删除一个之后个数不为0,再将其剩余个数加入multiset
            st.insert(x);
        if (y) //若此种数对删除一个之后个数不为0,再将其剩余个数加入multiset
            st.insert(y);
    }

    if (st.size() && k) //若经历了前面的过程之后k还没用完,且multiset不为空,说明还剩一种(两个数相邻且相同的数对)
    {
        int z = min(*st.begin(), k);
        //还剩下的同种的(两个数相邻且相同的数对)一次翻转只能减少一个,取翻转次数和剩下的数对个数中的较小值
        k -= z;
        ans += z; //更新答案
    }

    //答案最大不超过(n-maxnum)*2
    ans = min(ans, (n - maxnum) * 2);
    cout << ans << endl;
}

总结:

  1. 本题为对一个序列进行翻转操作的思维题

  2. 本题利用到了 STL 中的 vector、multiset 以及它们的若干操作来简化代码,可以多回顾

posted @ 2021-07-10 17:18  Wajor  阅读(106)  评论(0)    收藏  举报