【洛谷P4137】Rmq Problem / mex【主席树】

本文介绍使用主席树解决洛谷P4137题,即在一个动态变化的数组中,快速查询指定区间内最小未出现的自然数。通过构建线段树并维护每个数字最后一次出现的位置,实现O(n log n)的时间复杂度。

题目大意:

题目链接:https://www.luogu.org/problemnew/show/P4137
有一个长度为 n n n的数组 { a 1 , a 2 , … , a n } \{a1,a2,…,an\} {a1,a2,,an} m m m次询问,每次询问一个区间内最小没有出现过的自然数。


思路:

显然主席树。
对于第 k k k棵线段树,设叶子节点 i i i表示在数列前 k k k个数字中,数字 i i i出现的最后位置。
显然 a [ ] a[] a[]的范围是吓人的。最终答案一定不会 > n + 1 >n+1 >n+1,所以读入到 x ( x > n ) x(x>n) x(x>n)之后直接把 x x x变成 n + 1 n+1 n+1就可以了。
对于任意一个询问 x , y x,y x,y,意思就是说在第y棵线段树中找到第一个叶子结点最后出现的位置<l
可以在任意区间 [ l , r ] [l,r] [l,r]中记录 m i n min min表示 [ l , r ] [l,r] [l,r]中最后出现位置最前的位置。
那么对于任意区间,如果左儿子 m i n min min小于 l l l,答案就在左边,否则在右边。
时间复杂度 O ( n   l o g   n ) O(n\ log\ n) O(n log n)


代码:

#include <cstdio>
#include <algorithm>
using namespace std;

const int N=200010;
int n,T,tot,x,y,root[N];

struct Tree
{
	int ls,rs,last,min;
}tree[N+N*20];

int build(int l,int r)
{
	int p=++tot;
	if (l==r) return p;
	int mid=(l+r)/2;
	tree[p].ls=build(l,mid);
	tree[p].rs=build(mid+1,r);
	return p;
}

int insert(int now,int l,int r,int x,int val)
{
	int p=++tot;
	tree[p]=tree[now];
	if (l==r)
	{
		tree[p].last=tree[p].min=val;
		return p;
	}
	int mid=(l+r)/2;
	if (x<=mid) tree[p].ls=insert(tree[now].ls,l,mid,x,val);
		else tree[p].rs=insert(tree[now].rs,mid+1,r,x,val);
	tree[p].min=min(tree[tree[p].ls].min,tree[tree[p].rs].min);
	return p;
}

int ask(int x,int l,int r,int val)
{
	if (l==r) return r;  //找到答案
	int mid=(l+r)/2;
	if (tree[tree[x].ls].min>=val) return ask(tree[x].rs,mid+1,r,val);
		else return ask(tree[x].ls,l,mid,val); 
}

int main()
{
	scanf("%d%d",&n,&T);
	root[0]=build(0,n);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&x);
		if (x<=n) root[i]=insert(root[i-1],0,n,x,i);  //插入
			else root[i]=insert(root[i-1],0,n,n+1,i);
	}
	while (T--)
	{
		scanf("%d%d",&x,&y);
		printf("%d\n",ask(root[y],0,n,x)); 
	}
	return 0;
}
### 洛谷 P1816 题目解析 #### 问题描述 洛谷 P1816《忠诚》的题目背景设定在一个庄园中,老管家为财主工作了十年。为了验证管家的忠诚度,财主设计了一种特殊的测试方法。具体来说,财主要求管家记录每日账目,并对其进行查询操作。 输入数据分为两部分: - **账目记录**:每一天有若干次账目记录,每条记录有一个具体的数值。 - **查询请求**:每次查询指定一段区间 `[a, b]`,询问该区间的最小值。 目标是实现一个程序,在满足高效性和准确性的同时处理这些查询请求[^4]。 --- #### 解决方案分析 此题的核心在于如何快速响应大量的区间最小值查询。以下是两种常见的解决思路: ##### 方法一:暴力枚举法 对于每一个查询 `(a, b)`,可以直接遍历数组中的子区间 `[a, b]` 来找到其中的最小值。然而,这种方法的时间复杂度较高,达到 \(O(Q \times N)\),其中 \(Q\) 是查询次数,\(N\) 是账目的总数。当数据规模较大时,这种算法可能无法通过所有测试用例。 ```cpp #include <bits/stdc++.h> using namespace std; int main() { int n, q; cin >> n >> q; // 账目数量和查询次数 vector<int> accounts(n); for (int i = 0; i < n; ++i) { cin >> accounts[i]; } while (q--) { int a, b; cin >> a >> b; // 查询范围 [a, b] int min_val = INT_MAX; for (int i = a - 1; i <= b - 1; ++i) { // 注意索引偏移 if (accounts[i] < min_val) { min_val = accounts[i]; } } cout << min_val << endl; } } ``` 虽然简单易懂,但在大规模数据下效率低下[^4]。 --- ##### 方法二:预处理 + 倍增 RMQ(Range Minimum Query) 倍增 RMQ 是一种高效的解决方案,其核心思想是对原数组进行预处理,从而加速后续的查询过程。具体步骤如下: 1. 定义 `f[i][j]` 表示从位置 `i` 开始,长度为 \(2^j\) 的子区间的最小值。 2. 利用动态规划的思想计算所有的 `f[i][j]` 值: - 当 \(j = 0\) 时,`f[i][0] = nums[i]`; - 对于更大的 \(j\),可以由两个更小区间的结果合并得到: \[ f[i][j] = \min(f[i][j-1], f[i + 2^{j-1}][j-1]) \] 3. 在查询阶段,给定区间 `[a, b]`,可以通过分解成两个重叠的部分来快速得出答案。假设区间长度为 \(len = b - a + 1\),则令 \(k = \lfloor\log_2(len)\rfloor\),最终结果为: \[ \text{result} = \min(f[a][k], f[b - 2^k + 1][k]) \] 下面是完整的代码实现: ```cpp #include <bits/stdc++.h> using namespace std; const int MAX_N = 5e4 + 5; const int LOG = 17; // log2(50000) // 动态规划表 int f[MAX_N][LOG]; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; // 数组大小和查询次数 vector<int> nums(n); for (int i = 0; i < n; ++i) { cin >> nums[i]; // 输入账目数据 } // 初始化 dp 表 for (int i = 0; i < n; ++i) { f[i][0] = nums[i]; } // 计算更高层次的状态转移 for (int j = 1; j < LOG; ++j) { for (int i = 0; i + (1 << j) - 1 < n; ++i) { f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); } } // 处理查询 while (m--) { int l, r; cin >> l >> r; // 查询范围 [l, r] --l; --r; // 转换为零基索引 int k = floor(log2(r - l + 1)); cout << min(f[l][k], f[r - (1 << k) + 1][k]) << "\n"; } } ``` 这段代码利用倍增技术实现了高效的区间最值查询,时间复杂度降到了 \(O(N \log N + Q \log N)\)[^4]。 --- #### 总结 针对洛谷 P1816,《忠诚》这道题目提供了多种解法。如果追求简洁性,可以选择暴力枚举;但如果希望优化性能,则推荐采用倍增 RMQ 技术。后者不仅能够显著提升运行速度,还具有较高的通用性,适用于其他类似的区间查询场景。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值