洛谷 P3765 总统选举(线段树+treap)

本文介绍了如何运用线段树和Treap数据结构解决一道关于总统选举的算法题。题目要求在每次预选中找到区间内得票过半的候选人,若不存在则指定一个胜者。线段树用于维护区间内的票数信息,Treap用于验证最终结果。文章通过代码展示了如何实现这一过程,时间复杂度为O(Σki * logn)。

题目背景

黑恶势力的反攻计划被小C成功摧毁,黑恶势力只好投降。秋之国的人民解放了,举国欢庆。此时,原秋之国总统因没能守护好国土,申请辞职,并请秋之国人民的大救星小C钦定下一任。作为一名民主人士,小C决定举行全民大选来决定下一任。为了使最后成为总统的人得到绝大多数人认同,小C认为,一个人必须获得超过全部人总数的一半的票数才能成为总统。如果不存在符合条件的候选人,小C只好自己来当临时大总统。为了尽可能避免这种情况,小C决定先进行几次小规模预选,根据预选的情况,选民可以重新决定自己选票的去向。由于秋之国人数较多,统计投票结果和选票变更也成为了麻烦的事情,小C找到了你,让你帮他解决这个问题。


题目描述

秋之国共有n个人,分别编号为1,2,…,n,一开始每个人都投了一票,范围1~n,表示支持对应编号的人当总统。共有m次预选,每次选取编号[li,ri]内的选民展开小规模预选,在该区间内获得超过区间大小一半的票的人获胜,如果没有人获胜,则由小C钦定一位候选者获得此次预选的胜利(获胜者可以不在该区间内),每次预选的结果需要公布出来,并且每次会有ki个人决定将票改投向该次预选的获胜者。全部预选结束后,公布最后成为总统的候选人。


输入输出格式

输入格式:

第一行两个整数n,m,表示秋之国人数和预选次数。

第二行n个整数,分别表示编号1~n的选民投的票。

接下来m行,每行先有4个整数,分别表示li,ri,si,ki,si表示若此次预选无人胜选,视作编号为si的人获得胜利,接下来ki个整数,分别表示决定改投的选民。

输出格式:

共m+1行,前m行表示各次预选的结果,最后一行表示最后成为总统的候选人,若最后仍无人胜选,输出-1。


输入输出样例

输入样例#1:

5 4
1 2 3 4 5
1 2 1 1 3
5 5 1 2 2 4
2 4 2 0
3 4 2 1 4

输出样例#1:

1
5
5
2
-1


这里写图片描述


题解

这题呢,是一道线段树的大好题。首先我们明确题目就是想求一个区间内是否有出现次数大于区间长度一半的数,如果有,我想知道它是多少。这就是BZOJ 2456找众数那题的强化版。

题目不同之处在于支持修改,所以需要线段数维护,为什么线段树可以呢?
因为线段数能维护的东西有两点性质:

①维护的信息满足区间加法。就是说一个区间分为左右两个区间,这两个区间的信息是可加(即合并)的。例如区间和等于左区间和与右区间和的和,区间最值等于左区间最值与右区间最值的最值……
②能直接维护的信息一定是个封闭的域,即可以自己维护自己。这貌似是一句废话,我们明确知道当前区间一定能够维护想维护信息的准确结果。例如我们可以维护最值与和,因为这与其他区间无关,我们也可以维护区间内1的个数,也可以维护从左开始的最长连续的1的个数,从右边开始也行,但是不能直接维护整个区间的最长连续的1的个数。为什么呢?因为当两个区间合并时,就无法维护跨越两个区间的那部分了,就是说有一部分维护不了了。就这么简单,所以才要开三个数组来维护这个东西。

言归正传,这题我们可以用线段树维护val,cnt(定义和找众数一样),支持询问和修改。维护的具体细节就是两段val相同,cnt加起来,两段val不同cnt就大减小并将val设为cnt大的那段的val。

这样这题就做完了吗?并不。因为“众数”不一定存在,就是说未必有满足题目的答案,我们维护出的未必是正确答案,只是维护的那个数是“最具有成为‘众数’的潜质”的数罢了。所以和之前不同的,我们要开一个平衡树检验答案的正确性。很简单,就每个人开个treap判断在[l,r]内有几个人选他就行了。这只需要在treap上进行普通的二叉查找就行了。
时间复杂度O(Σki * logn)。


代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
<think>好的,我现在需要详细解释Splay树、Treap树、LCA、线段树和RMO这几个数据结构与算法,并且按照用户的要求用C++代码示例。首先,我需要逐个整理每个概念的基本原理、实现方法和应用场景,然后给出相应的代码片段。同时,还要确保在最后提出相关的问题。 首先,Splay树。它是一种自平衡二叉搜索树,通过splay操作将最近访问的节点移动到根,利用局部性原理提高效率。实现时需要旋转操作,包括zig、zag等。应用场景包括缓存和频繁访问相同数据的场景。我需要写一个Splay树的节点结构,以及splay和插入函数的代码示例。 接下来是Treap树,结合了二叉搜索树和堆的特性。每个节点有优先级,通过旋转维持堆性质。插入时先按BST插入,再按优先级旋转。代码示例应包括节点结构、旋转和插入操作。然后是LCA(最近公共祖先),常见算法有倍增法。预处理每个节点的2^k级祖先,通过二分查找找到LCA。需要实现预处理和查询的函数,代码结构可能包括计算深度、跳转祖先等步骤。 线段树用于区间查询和更新,构建树结构,每个节点代表一个区间。实现时需要建树、查询和更新函数。例如,区间求和的线段树,代码中包括节点结构、递归建树、查询和更新操作。RMO可能是区域管理办公室的缩写,但在数据结构和算法中不常见。可能需要用户确认具体指的是什么。这里假设是范围最小查询(RMQ),所以解释RMQ的稀疏表实现方法,预处理二维数组存储区间最小值,查询时找到区间长度对应的k值,比较两个区间的最小值。 在代码部分,需要注意语法正确性,比如结构体定义、函数参数、递归调用等。例如,Splay树的splay函数需要处理左旋、右旋等情况,而Treap的插入需要维护堆性质,可能涉及左右旋转。最后,相关问题需要围绕这些数据结构的特点、实现细节、应用场景以及相关算法展开。例如,询问Splay树的优势、Treap的优先级作用、LCA的应用、线段树的延迟标记等。</think>以下是C++相关数据结构和算法的详细解释及代码实现: 1. **Splay树(伸展树)** 自平衡二叉搜索树,通过"splay"操作将最近访问的节点移动到根位置 ```cpp struct SplayNode { int key; SplayNode *left, *right, *parent; SplayNode(int k) : key(k), left(nullptr), right(nullptr), parent(nullptr) {} }; void splay(SplayNode* x) { while (x->parent) { if (!x->parent->parent) { // Zig step if (x->parent->left == x) rotateRight(x->parent); else rotateLeft(x->parent); } else if ((x->parent->left == x) && (x->parent->parent->left == x->parent)) { // Zig-zig right rotateRight(x->parent->parent); rotateRight(x->parent); } else if ((x->parent->right == x) && (x->parent->parent->right == x->parent)) { // Zig-zig left rotateLeft(x->parent->parent); rotateLeft(x->parent); } else { // Zig-zag if (x->parent->left == x) { rotateRight(x->parent); rotateLeft(x->parent); } else { rotateLeft(x->parent); rotateRight(x->parent); } } } } ``` 2. **Treap(树堆)** 结合二叉搜索树和堆特性的概率平衡树 ```cpp struct TreapNode { int key, priority; TreapNode *left, *right; TreapNode(int k) : key(k), priority(rand()), left(nullptr), right(nullptr) {} }; TreapNode* rotateRight(TreapNode* y) { TreapNode* x = y->left; y->left = x->right; x->right = y; return x; } TreapNode* insert(TreapNode* root, int key) { if (!root) return new TreapNode(key); if (key < root->key) { root->left = insert(root->left, key); if (root->left->priority > root->priority) root = rotateRight(root); } else { root->right = insert(root->right, key); if (root->right->priority > root->priority) root = rotateLeft(root); } return root; } ``` 3. **LCA(最近公共祖先)** 使用倍增法实现的LCA算法 ```cpp const int MAX = 1000, LOG = 10; int depth[MAX], parent[MAX][LOG]; void preprocess(vector<int> tree[], int u, int p) { parent[u][0] = p; depth[u] = depth[p] + 1; for (int i = 1; i < LOG; ++i) parent[u][i] = parent[parent[u][i-1]][i-1]; for (int v : tree[u]) if (v != p) preprocess(tree, v, u); } int lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); // 提升较深的节点 for (int i = LOG-1; i >= 0; --i) if (depth[u] - (1<<i) >= depth[v]) u = parent[u][i]; if (u == v) return u; // 同时提升两个节点 for (int i = LOG-1; i >= 0; --i) if (parent[u][i] != parent[v][i]) { u = parent[u][i]; v = parent[v][i]; } return parent[u][0]; } ``` 4. **线段树** 区间查询数据结构示例(区间求和) ```cpp const int MAXN = 1e5; int arr[MAXN], tree[4*MAXN]; void build(int node, int start, int end) { if (start == end) { tree[node] = arr[start]; } else { int mid = (start + end) / 2; build(2*node, start, mid); build(2*node+1, mid+1, end); tree[node] = tree[2*node] + tree[2*node+1]; } } int query(int node, int start, int end, int l, int r) { if (r < start || end < l) return 0; if (l <= start && end <= r) return tree[node]; int mid = (start + end) / 2; return query(2*node, start, mid, l, r) + query(2*node+1, mid+1, end, l, r); } void update(int node, int start, int end, int idx, int val) { if (start == end) { arr[idx] = val; tree[node] = val; } else { int mid = (start + end) / 2; if (start <= idx && idx <= mid) update(2*node, start, mid, idx, val); else update(2*node+1, mid+1, end, idx, val); tree[node] = tree[2*node] + tree[2*node+1]; } } ``` 5. **RMO(Range Minimum Query)** 范围最小查询的稀疏表实现 ```cpp const int MAXN = 1e5, LOG = 17; int st[MAXN][LOG], log_table[MAXN]; void buildRMQ(int arr[], int n) { log_table[1] = 0; for (int i = 2; i <= n; ++i) log_table[i] = log_table[i/2] + 1; for (int i = 0; i < n; ++i) st[i][0] = arr[i]; for (int j = 1; j <= LOG; ++j) for (int i = 0; i + (1 << j) <= n; ++i) st[i][j] = min(st[i][j-1], st[i + (1 << (j-1))][j-1]); } int queryRMQ(int l, int r) { int k = log_table[r - l + 1]; return min(st[l][k], st[r - (1 << k) + 1][k]); } ```
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值