牛客网 牛牛的mex(思维题)

本文介绍一种解决区间查询问题的高效算法,对于n个唯一整数,快速找到指定区间外未出现的最小整数。通过维护前缀和后缀最小值,实现O(1)查询复杂度。

题目链接

题意大体是有n个数,n个数都在0和n-1之中且不会重复,现在有n个查询,每个查询会提供一个区间,要求输出这个区间内没出现过的数中最小的那个。

由于n个数是在0到n-1之前且不会重复,那么在区间内没出现过的数字一定就会在区间外部出现,所以转换一下思路,不关注区间内没有什么数字,转而去关注区间外有什么数字,很容易发现,区间外出现过的最小的数字就是答案。

求区间外出现过的最小的数字用一个前缀最小值和一个后缀最小值就能解决,这题的关键是关注点从区间内转向区间外。

#include <bits/stdc++.h>

using namespace std;
int n,q;
int pre[100010],suf[100010],num[100010];
int main()
{
    scanf("%d%d",&n,&q);
    pre[0]=suf[n+1]=n;
    for(int i=1;i<=n;i++)
        scanf("%d",&num[i]);
    for(int i=1;i<=n;i++)
        pre[i]=min(pre[i-1],num[i]);
    for(int i=n;i>=1;i--)
        suf[i]=min(suf[i+1],num[i]);
    while(q--)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",min(pre[x-1],suf[y+1]));
    }
    return 0;
}

你说得对!这道其实是一个 **思维(greedy + 数学观察)**,并不需要复杂的动态规划或状态压缩。我们可以通过观察问的性质,找到一个更简洁、高效的解法。 --- ## ✅ 问再理解 我们可以执行任意次操作,每次操作是: - 选择一个子集 $ S' \subseteq S $,然后: - **要么** 得到 `sum(S')` 的分数,并将这些元素从 $ S $ 中删除; - **要么** 得到 `mex(S')` 的分数,并将这些元素从 $ S $ 中删除。 我们的目标是最大化总得分。 --- ## ✅ 关键观察 ### 观察 1:mex(S') 的最大可能值是 `count of 0~k` mex 是最小的不在集合中的非负整数。如果我们有 `0~k-1` 都存在,但 `k` 不存在,则 mex 是 `k`。 所以为了获得较大的 mex,我们希望子集包含尽可能多的连续小整数。 ### 观察 2:每次操作应该尽可能“最大化”当前操作的收益 由于操作可以任意顺序执行,我们可以贪心地: - 每次从当前集合中选出一个子集,使得 `max(sum(S'), mex(S'))` 最大; - 然后删除这个子集; - 重复这个过程直到集合为空。 ### 观察 3:最优策略是贪心选择 mex 我们可以证明,**使用 mex 操作比 sum 更优**,如果我们能构造出一个 mex 较大的子集。 --- ## ✅ 更优的解法:贪心构造 mex 我们维护一个变量 `current_mex = 0`,表示当前 mex 的目标。 我们从 `0` 开始,尝试构造出一个子集包含所有 `0 ~ current_mex - 1`,并至少有一个元素。 - 如果我们有 `current_mex` 这个数,我们就可以将它加入子集,并继续增加 `current_mex`。 - 如果我们没有 `current_mex`,我们就不能继续构造连续的 mex,此时我们就可以将当前构造的子集删除,得到 `current_mex` 的得分。 - 重复这个过程,直到所有元素都被删除。 --- ## ✅ 代码实现(C++): ```cpp #include <bits/stdc++.h> using namespace std; int maxScore(vector<int>& S) { map<int, int> freq; for (int x : S) freq[x]++; int score = 0; int mex = 0; while (true) { // 构造 mex 子集 vector<int> to_remove; bool can_increase = false; // 如果当前 mex 存在,则可以构造 mex 子集 if (freq.count(mex) && freq[mex] > 0) { // 我们要构造一个子集包含 0~mex-1 各至少一个 bool valid = true; for (int i = 0; i < mex; ++i) { if (!freq.count(i) || freq[i] == 0) { valid = false; break; } } if (valid) { // 可以构造 mex 子集,得到 mex 分 score += mex; // 删除 0~mex-1 各一个 for (int i = 0; i < mex; ++i) { freq[i]--; if (freq[i] == 0) freq.erase(i); } freq[mex]--; if (freq[mex] == 0) freq.erase(mex); mex = 0; // 重新开始构造 continue; } } // 否则,尝试找一个可以增加 mex 的值 for (int i = mex; i <= 50; ++i) { if (freq.count(i) && freq[i] > 0) { mex = i; break; } } // 如果 mex == 51,说明没有元素了 if (mex > 50) break; // 否则,构造 mex 子集失败,直接删除 mex 的一个实例 score += mex; freq[mex]--; if (freq[mex] == 0) freq.erase(mex); mex = 0; } return score; } // 主函数 int main() { int t; cin >> t; while (t--) { int n; cin >> n; vector<int> S(n); for (int i = 0; i < n; ++i) cin >> S[i]; cout << maxScore(S) << endl; } return 0; } ``` --- ## ✅ 代码解释 - 我们用 `map<int, int>` 来维护当前每个数的出现次数。 - 每次尝试构造一个包含 `0 ~ mex-1` 的子集,如果能构造成功,就删除这些元素,并得分为 `mex`。 - 否则,我们找一个最小的存在的数作为 mex,删除它的一个实例。 - 重复这个过程直到没有元素为止。 --- ## ✅ 示例 假设输入: ``` 1 4 0 1 1 3 ``` - 第一次 mex=0,存在,尝试构造 `mex=0` 子集(不需要任何元素),得分 +0。 - 删除 0,剩下 [1,1,3] - mex=0,不存在,找最小存在的数是 1,删除一个,得分 +1 - mex=0,不存在,找最小存在的数是 1,删除一个,得分 +1 - mex=0,不存在,找最小存在的数是 3,删除一个,得分 +3 - 总得分:0+1+1+3 = 5 --- ## ✅
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值