蓝桥杯2024年第十五届省赛A组-封印宝石

题目描述

在一次探险中,勇者小蓝发现了 n 颗闪烁着奇异光芒的宝石,每颗宝石都蕴含着魔法能量,分别记作 a1, a2, . . . , an。小蓝计划用 n 个特制的魔法盒子来封印这些宝石,防止其魔法能量被滥用。

封印宝石会消耗小蓝的体力,具体地,将第 i 颗宝石放入第 j 个盒子会消耗小蓝 i − j 点体力(注:需满足 j ≤ i 才能将第 i 颗宝石放入第 j 个盒子进行有效的封印)。小蓝也可以选择将魔法盒留空,以保存体力供后续使用。

此外,为了避免魔力相冲,每个盒子最多存放一颗宝石(每个宝石也只能放进一个盒子),且任意两个相邻盒子不能存放魔力值相同的宝石,相邻的盒子允许同时为空。小蓝初始的体力值为 k。在不超出体力限制的条件下,小蓝希望找出一种宝石的放置方法,使得宝石的魔力值在这 n 个盒子中的排列顺序具有最大的字典序(注:未放置宝石的盒子在此序列中记为 −1)。

作为勇者小蓝的追随者,请你帮他找出这一放置宝石的方法。

字典序的解释: 在本题中,字典序的大小是按照宝石的魔力值进行比较的。对于两个长度同为 L 的魔力值序列 a 和 b,如果存在一个位置 i,使得aj = bj 对所有 1 ≤ j < i 成立,但是 ai < bi,则序列 a 在字典序上小于序列 b。反之,如果 ai > bi,则序列 a 在字典序上大于序列 b。如果不存在这样的 i,则序列 a 和序列 b 的字典序相等。

输入格式

输入的第一行包含两个整数 n 和 k ,用一个空格分隔,分别表示宝石的数量和小蓝的初始体力值。

第二行包含 n 个整数 a1, a2, · · · , an ,相邻整数之间使用一个空格分隔,分别表示每颗宝石的魔法能量值。

输出格式

输出一行包含 n 个整数,相邻整数之间使用一个空格分隔,表示每个魔法盒中宝石的魔法能量值。如果某个魔法盒为空,则对应位置输出 −1 。

样例输入

3 3
1 3 2

样例输出

3  2  -1

提示

【样例说明】

在开始放置宝石之前,体力为 3,宝石在盒子中的排列为 [−1, −1, −1]。1. 将第 2 个宝石放进第 1 个盒子,得到 [3, −1, −1],体力剩余 2。2. 将第 3 个宝石放进第 2 个盒子,得到 [3, 2, −1],体力剩余 1。最后宝石在盒子中的排列为 [3, 2, −1]。显然,没有比这更优的放置方法。

【评测用例规模与约定】

对于 20% 的评测用例,1 ≤ n ≤ 5 × 10^3 ,0 ≤ k ≤ 3 × 10^6 ,1 ≤ ai ≤ 10^5。对于所有评测用例,1 ≤ n ≤ 10^5 ,0 ≤ k ≤ 10^9 ,1 ≤ ai ≤ 10^9。

错误解法

看到字典序,很容易想到使用贪心的思想解决,利用滑动窗口寻找满足条件能量最高的宝石放到最前面的盒子,并不断更新体力值。

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;

const int N = 1e5 + 5;
int a[N], b[N]; // 宝石和魔法盒子 
set<pair<int, int>> s; // 存储可选宝石的集合,pair为<负魔法值, 索引> (保证魔法值递减,索引递增)

int main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, k;
	cin >> n >> k;
	for(int i = 1; i <= n; i++)
	{
		cin >> a[i];
	}
	
	// i 为宝石的指针,j 为盒子的指针 
	for(int i = 1, j = 1; j <= n; j++)
	{
		while(i <= n && i - j <= k)
		{
			if(a[i] != -1)
			{
				s.insert(make_pair(-a[i], i));
			}
			i++;
		}
		// 没有满足条件的宝石 
		if(s.empty())
		{
			b[j] = -1;
			cout << -1 << " ";
		}
		else
		{
			int idx = s.begin()->second; // 能量最高且最靠前的宝石的索引 
			if(a[idx] != b[j - 1])
			{
				cout << a[idx] << " ";
				s.erase(make_pair(-a[idx], idx));
				b[j] = a[idx];
				a[idx] = -1;
				k -= (idx - j);
			}
			else
			{
				idx = (++s.begin())->second;
				cout << a[idx] << " ";
				s.erase(make_pair(-a[idx], idx));
				b[j] = a[idx];
				a[idx] = -1;
				k -= (idx - j);
			}
		}
		s.erase(make_pair(-a[j], j)); // 保证 j<=i,盒子前的宝石不考虑 
		// 保证宝石选取在小蓝的体力范围内 
		while(i - j - 1 > k)
		{
			s.erase(make_pair(-a[i], i));
			i--;
		}
	}
	return 0;
}

集合 s 只存储当前位置 i 到 i+k 的宝石,最多含有 k 个元素。而 set 底层是由红黑树实现的,所以插入和删除的时间复杂度均为 O(log k)。加上访问 s 的时间复杂度是 O(n),整体时间复杂度是 O(n log k)。由于 k >> n,根据计算会有超时的可能。

整体思路

使用贪心的思想,并用线段树维护数组的最大值和次最大值。每次选最大的一个值放到前面,同时为了节省体力,需要选相同的这个最大值最前面的一个,放入后进行删除操作。

注:使用线段树让时间复杂度降为 O(n log n),n 的范围是 10^5,所以不会超时。

#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define lc p << 1
#define rc p << 1 | 1
using namespace std;

const int N = 1e5 + 5;
int a[N], ans[N]; // 宝石和魔法盒子  
struct node
{
	int id, val; // 索引和数值 
	node() {val = id = 0;}
}zero;
struct tree
{
	int l, r;
	struct node mx1, mx2; // 范围内最大值与次最大值 
}tr[N << 2];

bool cmp(node x, node y)
{
	if(x.val == y.val)
	{
		return x.id > y.id;
	}
	return x.val < y.val;
}

inline void pushup(int p)
{
	struct node tmp[4];
	tmp[0] = tr[lc].mx1;
	tmp[1] = tr[lc].mx2;
	tmp[2] = tr[rc].mx1;
	tmp[3] = tr[rc].mx2;
	sort(tmp, tmp + 4, cmp);
	tr[p].mx1 = tmp[3];
	for(int i = 2; i >= 0; i--)
	{
		if(tmp[i].val != tmp[i + 1].val)
		{
			tr[p].mx2 = tmp[i];
			break;
		}
	}
}

inline void build(int p, int l, int r)
{
	tr[p].l = l;
	tr[p].r = r;
	if(l == r)
	{
		tr[p].mx1.val = a[l];
		tr[p].mx1.id = l;
		return;
	}
	int mid = (l + r) >> 1;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(p);
}

// 寻找最大值与次最大值
node m1, m2; 
inline void query(int p, int x, int y)
{
	if(x <= tr[p].l && tr[p].r <= y)
	{
		struct node tmp[4];
		tmp[0] = tr[p].mx1;
		tmp[1] = tr[p].mx2;
		tmp[2] = m1;
		tmp[3] = m2;
		sort(tmp, tmp + 4, cmp);
		m1 = tmp[3];
		for(int i = 2; i >= 0; i--)
		{
			if(tmp[i].val != tmp[i + 1].val)
			{
				m2 = tmp[i];
				break;
			}
		}
		return;
	}
	int mid = (tr[p].l + tr[p].r) >> 1;
	if(x <= mid)
	{
		query(lc, x, y);
	}
	if(y > mid)
	{
		query(rc, x, y);
	}
}

// 标记用过的宝石 
inline void update(int p, int x)
{
	if(tr[p].l == x && tr[p].r == x)
	{
		tr[p].mx1 = tr[p].mx2 = zero;
		return;
	}
	int mid = (tr[p].l + tr[p].r) >> 1;
	if(x <= mid)
	{
		update(lc, x);
	}
	if(x > mid)
	{
		update(rc, x);
	}
	pushup(p);
}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, k;
	cin >> n >> k;
	for(int i = 1; i <= n; i++)
	{
		cin >> a[i];
	}
	build(1, 1, n);
	for(int i = 1; i <= n; i++)
	{
		m1 = m2 = zero;
		query(1, i, min(i + k, n));
		if(m1.val == ans[i - 1])
		{
			if(m2.val != 0)
			{
				ans[i] = m2.val;
				update(1, m2.id);
				k -= (m2.id - i);
			}
		}
		if(m1.val != ans[i - 1] && m1.val != 0)
		{
			ans[i] = m1.val;
			update(1, m1.id);
			k -= (m1.id - i);
		}
	}
	for(int i = 1; i <= n; i++)
	{
		cout << (ans[i] ? ans[i] : -1) << " ";
	}
	return 0;
}

具体步骤

1. 按要求读入数据,递归构建线段树,并维护区间内最靠前的最大值和次最大值。

2. 在当前位置 i 到 i + k 的范围内寻找最值,填入魔法盒子内,并不断更新宝石状态。

3. 若盒子为空,按要求输出 -1,否则输出盒子内的值。

  • 17
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值