洛谷 P1440 求m区间内的最小值

该博客讨论了一种使用优化的RMQ(Range Minimum Query)算法来解决在数列中找到每个元素前m个数的最小值的问题。当m小于或等于n时,通过倍增思想更新数组,避免了空间超限。对于n-m个元素,利用特定公式计算最小值。博主提供了80分的解题代码,并提到使用zkw线段树也可以解决问题,但效率较低。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

    一个含有n项的数列(n<=2000000),求出每一项前的m个数到它这个区间内的最小值。若前面的数不足m项则从第1个数开始,若前面没有数则输出0

输入格式:

    第一行两个数nm

    第二行,n个正整数,为所给定的数列。

输出格式:

    n行,第i行的一个数ai,为所求序列中第i个数前m个数的最小值。

输入样例

    6 2

    7 8 1 43 2

输出样例

    0

    7

    7

    1

    1

    3

数据规模m≤n≤2000000

解题思路:

        这道题目要用RMQ的优化版。纯的RMQ内存空间是要超限的。我们只需要开一维数组即可。通过使用倍增的思想从而达到时间不超限的目的。先输出0,作为第一个答案,再输出a[1]。后面的数据我们先a[1]=min(a[1],a[2]),a[2]=min(a[2],a[3])……,再a[1]=min(a[1],a[1+2^1]), a[2]=min(a[2],a[2+2^1])……,再a[1]=min(a[1],a[1+2^2]), a[2]=min(a[2],a[2+2^2])……这样不断的求最小值,即可求得答案(当然,这是i<m的部分)。最后的n-m的部分,我们只需输出min(a[i],a[i+m-2^trunc(log2(m))])即可。

      这道题目用纯的线段树是要时间超时的,能拿到80分,下面我提供一个80分的代码。

      这道题目使用zkw线段树是可以通过的,不过比RMQ的优化版要再耗时一些,具体见第三个代码。

代码:(请不要直接拷贝哦)

//RMQ
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
int n,m,k,l,p,a[2000001];
using namespace std;
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	printf("0\n");//输出第一个答案
	if (m>1) printf("%d\n",a[1]);
	k=log2(m),l=2,p=1;
	for (int i=1;i<=k;i++)
	{
		p*=2;//倍增开始
		for (int j=1;j<=n-p+1;j++)
		  a[j]=min(a[j],a[j+p/2]);//不断求区间最小值
		while ((l<=p*2)&&(l<m))
		{
			printf("%d\n",min(a[1],a[l-p+1]));//在前m的部分,我们可以这样输出答案
			l++;
		} 
	}
	for (int i=1;i<=n-m;i++)
	  printf("%d\n",min(a[i],a[i+m-p]));//在m+1到n的部分,我们要另外考虑
	return 0;
}
//80分 纯线段树
#include <cstdio>
using namespace std;
struct TREE{
	int l,r,min;	
}tree[8000001];
int n,m,a[4000001];
inline void build(int root,int l,int r)
{
	tree[root].l=l;
	tree[root].r=r;
	if (l==r) tree[root].min=a[l]; else
	{
		build(root*2,tree[root].l,(l+r)/2);
		build(root*2+1,(l+r)/2+1,tree[root].r);
		if (tree[root*2].min<tree[root*2+1].min)
		  tree[root].min=tree[root*2].min; else
		  tree[root].min=tree[root*2+1].min;
	}
}
inline int find(int root,int l,int r)
{
	if ((tree[root].l==l)&&(tree[root].r==r))
	  return tree[root].min;
	if (r<=(tree[root].l+tree[root].r)/2) return find(root*2,l,r);
	  else if (l>(tree[root].l+tree[root].r)/2) return find(root*2+1,l,r);
	  else
	  {
	  	int x=find(root*2,l,(tree[root].l+tree[root].r)/2);
	  	int y=find(root*2+1,(tree[root].l+tree[root].r)/2+1,r);
	  	return x<y?x:y;
	  }
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	build(1,1,n);
	printf("0\n");
	for (int i=2;i<=m+1;i++)
	  printf("%d\n",find(1,1,i-1));
	for (int i=m+2;i<=n;i++)
	  printf("%d\n",find(1,i-m,i-1));
	return 0;
}
//100分 zkw线段树
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,bit,tree[8000005];
int main()
{
    scanf("%d%d",&n,&m);
    for(bit=1;bit<=n+1;bit<<=1);
    for(int i=bit+1;i<=bit+n;i++) scanf("%d",&tree[i]);
    for(int i=bit-1;i;i--)
        tree[i]=min(tree[i<<1],tree[i<<1|1]);//建树
    puts("0");
    for(int i=2;i<=n;++i)
    {
        int L=max(bit,i-m-1+bit),R=bit+i;//开区间查询
        int ans=2e9;
        for(;L!=(R^1);L>>=1,R>>=1){//直到移到L和R为相邻的兄弟节点   
        if((L&1)==0) ans=min(ans,tree[L+1]);//如果L为左儿子,那么它的右兄弟肯定在区间内   
        if((R&1)==1) ans=min(ans,tree[R-1]);//如果R为右儿子,那么它的左兄弟肯定在区间内 
        }
        printf("%d\n",ans);
    }
    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、付费专栏及课程。

余额充值