Codeforces Round 1054 (Div. 3)-G. Buratsuta 3

题目链接

这道题目可以说是很典的一个题目,解法很多,适合练习,这里给出随机选数、摩尔投票、主席树,三种解法。

题意:给定一个数组,有q次询问,每次询问该数组的一个区间[ l ,r ] ,让你找出其中出现超过 \lfloor\frac{r-l+1}{3}\rfloor 的数,并且输出。

数据范围:

代码一(随机选数)

就是对于一个区间中的数字,每次随机选取一个数字,二分统计这个区间中数字出现的次数,我们随机50次,出错的概率可以小到  \left(\frac{2}{3}\right)^{50} \approx 0.000000001568

时间复杂度:O(n \log n + 50q \log n)

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int n,q;
int a[200005],b[200005];
vector<int> v[200005];
mt19937 rnd(time(0));
int query(int l,int r,int x)
{
	return upper_bound(v[x].begin(),v[x].end(),r)-lower_bound(v[x].begin(),v[x].end(),l);
}
void test()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)
		cin>>a[i],b[i]=a[i];
	sort(b+1,b+n+1);
	int m=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(b+1,b+m+1,a[i])-b;
	for(int i=1;i<=m;i++)
		v[i].clear();
	for(int i=1;i<=n;i++)
		v[a[i]].push_back(i);
	for(int i=1;i<=q;i++)
	{
		int l,r;
		cin>>l>>r;
		int k=(r-l+1)/3;
		set<int> st;
		for(int j=1;j<=50;j++)
		{
			int x=rnd()%(r-l+1)+l;
			if(query(l,r,a[x])>k)
				st.insert(b[a[x]]);
			if(st.size()>=3)
				break;
		}
		for(int x:st)
			cout<<x<<" ";
		if(st.empty())
			cout<<-1;
		cout<<"\n";
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	cin>>t;
	for(int i=1;i<=t;i++)
		test();
}

代码二(主席树)

主席树一般解决区间第k小问题,在这里其实就是求某区间出现次数大于k的数

重点理解query逻辑就行,目的是找到区间中某一个点的计数 > k ,而不是每次计算一个区间中的数了,但是只有当某个区间中的cnt > k ,我们要找的点才可能存在于这样的区间 ,然后不断收缩,直到找到 l == r,或者直接路径中断,并用fg标记有没有找到解,没有找到的话就输出-1,找到的话就直接输出,其余就是主席树模板。

核心代码:$O(n \log n + q \log n)$

void query_k(int u, int v, int l, int r, int k)
{
    if (l == r) // 最后收缩到一个点 说明这个点的数量  > k 
    {
        fg = 1;
        cout << nums[l - 1] << " ";
        return;
    }

    int diff_l = tr[tr[u].l].cnt - tr[tr[v].l].cnt;
    int diff_r = tr[tr[u].r].cnt - tr[tr[v].r].cnt;

    int mid = l + r >> 1;
    if (k < diff_l) // 右子树找不到再往左子树中找
    {
        // 第 k 小的数在左子树
        query_k(tr[u].l, tr[v].l, l, mid, k);
    }
    if (k < diff_r)
    {
        // 第 k 小的数在右子树
        query_k(tr[u].r, tr[v].r, mid + 1, r, k);
    }
}

理解这个直接套模板就行:

时间复杂度:$O(n \log n + q \log n)$

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 200005; // 序列长度和查询次数的最大值
vector<int> v[N];
int n, m, fg;
int a[N];         // 原始序列
vector<int> nums; // 用于存储所有待离散化的值

// 获取 x 离散化后的排名
int get_id(int x)
{
    return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
}

struct Node
{
    int l, r; // 左右子节点的编号
    int cnt;  // 区间内数的个数
} tr[N * 24]; // 空间大小估算: n * log(n) + m * log(n),开大一些比较保险
int root[N];  // root[i] 存储第 i 个版本的根节点
int tot = 0;  // 节点总数

int insert(int p, int l, int r, int x)
{
    int q = ++tot; // 创建一个新节点 q
    tr[q] = tr[p]; // 复制上一个版本的信息
    tr[q].cnt++;   // 当前区间的计数值+1
    if (l == r)
        return q; // 到达叶子节点,返回
    int mid = l + r >> 1;
    if (x <= mid)
    {
        // 如果 x 在左半区间,递归更新左子树,并把新左子树的根节点赋给 q->l
        tr[q].l = insert(tr[p].l, l, mid, x);
    }
    else
    {
        // 否则递归更新右子树
        tr[q].r = insert(tr[p].r, mid + 1, r, x);
    }
    return q; // 返回新节点的编号
}

void query_k(int u, int v, int l, int r, int k)
{
    if (l == r) // 最后收缩到一个点 说明这个点的数量  > k 
    {
        fg = 1;
        cout << nums[l - 1] << " ";
        return;
    }

    int diff_l = tr[tr[u].l].cnt - tr[tr[v].l].cnt;
    int diff_r = tr[tr[u].r].cnt - tr[tr[v].r].cnt;

    int mid = l + r >> 1;
    if (k < diff_l) // 右子树找不到再往左子树中找
    {
        // 第 k 小的数在左子树
        query_k(tr[u].l, tr[v].l, l, mid, k);
    }
    if (k < diff_r)
    {
        // 第 k 小的数在右子树
        query_k(tr[u].r, tr[v].r, mid + 1, r, k);
    }
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;
    while (t--)
    {
        tot = 0;
        nums.clear();
        cin >> n >> m;
        for (int i = 1; i <= n; ++i)
        { 
            root[i] = 0;
            v[i].clear();
        }
        for (int i = 1; i <= n; ++i)
        {
            cin >> a[i];
            nums.push_back(a[i]);
        }
        // 离散化
        sort(nums.begin(), nums.end());
        nums.erase(unique(nums.begin(), nums.end()), nums.end());
        int range = nums.size();
        for (int i = 1; i <= n; ++i)
        {
            int id = get_id(a[i]);
            root[i] = insert(root[i - 1], 1, range, id);
        }
        // 处理 m 次查询
        for (int i = 0; i < m; ++i)
        {
            fg = 0;
            int l, r;
            set<int> st;
            cin >> l >> r;
            int k = (r - l + 1) / 3;
            query_k(root[r], root[l - 1], 1, range, k);
            if (!fg)
                cout << -1;
            cout << endl;
        }
    }
    return 0;
}

代码三(摩尔投票+线段树)

摩尔投票相比前两者有些麻烦,感兴趣的可以了解一下。

时间复杂度:$O(n \log n + q \log n)$

#include <iostream>
#include <vector>
#include <algorithm>
#include <map>

using namespace std;

// 线段树节点结构体
struct Node {
    pair<int, int> c[2]; // 存储两个候选数 {值id, 票数}
    
    Node() {
        c[0] = {0, 0}; // 初始化两个候选数为空
        c[1] = {0, 0};
    }
};

// 全局变量
vector<int> arr;      // 存储原始数组
vector<Node> seg;     // 线段树
vector<vector<int>> pos; // 每个值出现的位置列表
map<int, int> val2id; // 值到离散化id的映射
vector<int> id2val;   // 离散化id到值的映射

// 合并两个节点的候选信息
Node merge(const Node &l, const Node &r) {
    Node res = l; // 以左节点为基础
    
    // 处理右节点的两个候选数
    for (int i = 0; i < 2; ++i) {
        if (r.c[i].second == 0) continue; // 跳过空候选
        
        int val = r.c[i].first;   // 候选数值id
        int cnt = r.c[i].second;  // 候选数票数
        
        // 情况1: 候选数已在res中
        if (res.c[0].first == val) {
            res.c[0].second += cnt;
        } 
        // 情况2: 候选数在res的第二个位置
        else if (res.c[1].first == val) {
            res.c[1].second += cnt;
        } 
        // 情况3: res有空位
        else if (res.c[0].second == 0) {
            res.c[0] = {val, cnt};
        } 
        else if (res.c[1].second == 0) {
            res.c[1] = {val, cnt};
        } 
        // 情况4: 需要三方抵消
        else {
            // 计算最小票数进行抵消
            int minv = min({res.c[0].second, res.c[1].second, cnt});
            res.c[0].second -= minv;
            res.c[1].second -= minv;
            cnt -= minv;
            
            // 抵消后如果还有剩余票数,尝试占据空位
            if (cnt > 0) {
                if (res.c[0].second == 0) {
                    res.c[0] = {val, cnt};
                } else if (res.c[1].second == 0) {
                    res.c[1] = {val, cnt};
                }
            }
        }
    }
    
    // 清理票数为0的候选
    if (res.c[0].second == 0) res.c[0].first = 0;
    if (res.c[1].second == 0) res.c[1].first = 0;
    
    // 确保票数多的在前
    if (res.c[0].second < res.c[1].second) {
        swap(res.c[0], res.c[1]);
    }
    
    return res;
}

// 构建线段树
void build(int u, int l, int r) {
    if (l == r) { // 叶子节点
        seg[u].c[0] = {arr[l], 1}; // 初始候选是自己,票数为1
        seg[u].c[1] = {0, 0};      // 第二个候选为空
        return;
    }
    int mid = (l + r) / 2;
    build(u * 2, l, mid);      // 构建左子树
    build(u * 2 + 1, mid + 1, r); // 构建右子树
    seg[u] = merge(seg[u * 2], seg[u * 2 + 1]); // 合并子节点信息
}

// 区间查询
Node query(int u, int l, int r, int ql, int qr) {
    if (qr < l || r < ql) return Node(); // 查询区间与当前区间无交集
    if (ql <= l && r <= qr) return seg[u]; // 当前区间完全包含在查询区间内
    
    int mid = (l + r) / 2;
    Node left = query(u * 2, l, mid, ql, qr);    // 查询左子树
    Node right = query(u * 2 + 1, mid + 1, r, ql, qr); // 查询右子树
    return merge(left, right); // 合并左右子树结果
}

// 统计某个值在区间[l,r]内的出现次数
int count_in_range(int id, int l, int r) {
    if (id == 0) return 0; // 无效id
    auto &p = pos[id];     // 获取该值的所有位置
    // 使用二分查找统计在[l,r]范围内的数量
    auto itl = lower_bound(p.begin(), p.end(), l);
    auto itr = upper_bound(p.begin(), p.end(), r);
    return distance(itl, itr);
}

void solve() {
    int n, q;
    cin >> n >> q;
    
    // 读取并离散化数据
    arr.assign(n + 1, 0);
    vector<int> tmp(n);
    for (int i = 1; i <= n; ++i) {
        cin >> arr[i];
        tmp[i - 1] = arr[i];
    }
    
    // 去重排序
    sort(tmp.begin(), tmp.end());
    tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end());
    
    // 建立双向映射
    val2id.clear();
    id2val.assign(tmp.size() + 1, 0);
    for (size_t i = 0; i < tmp.size(); ++i) {
        val2id[tmp[i]] = i + 1; // 值到id
        id2val[i + 1] = tmp[i]; // id到值
    }
    
    // 记录每个值的位置
    pos.assign(tmp.size() + 1, vector<int>());
    for (int i = 1; i <= n; ++i) {
        arr[i] = val2id[arr[i]]; // 将值转换为id
        pos[arr[i]].push_back(i); // 记录位置
    }
    
    // 构建线段树
    seg.assign(4 * (n + 1), Node());
    build(1, 1, n);
    
    // 处理查询
    for (int i = 0; i < q; ++i) {
        int l, r;
        cin >> l >> r;
        Node res = query(1, 1, n, l, r); // 查询区间候选
        int len = r - l + 1;
        int thr = len / 3; // 阈值
        
        // 验证候选是否满足条件
        vector<int> ans;
        if (res.c[0].second > 0 && count_in_range(res.c[0].first, l, r) > thr) {
            ans.push_back(id2val[res.c[0].first]);
        }
        if (res.c[1].second > 0 && count_in_range(res.c[1].first, l, r) > thr) {
            ans.push_back(id2val[res.c[1].first]);
        }
        
        // 去重排序输出
        sort(ans.begin(), ans.end());
        ans.erase(unique(ans.begin(), ans.end()), ans.end());
        
        if (ans.empty()) {
            cout << -1 << "\n";
        } else {
            for (size_t j = 0; j < ans.size(); ++j) {
                cout << ans[j] << (j == ans.size() - 1 ? "" : " ");
            }
            cout << "\n";
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值