2024航电多校3(1,7,8,11,12)细述7,8.

1012死亡之组

题意:给出一个4的倍数的n,以及n个队伍的实力,问什么取法能使得1号所在的小组不是死亡之组,即球队里至少有3个队伍实力小于L且最大的和最小的差不超过D。

思路:排序然后取第一个小组后尽可能多的取满3个小于L的小组,最后一个尽可能取最大的满足第二个条件,模拟一下即可。

1001深度自同构

题意:找出x点构成的满足题目条件的森林有几个,深度自同构指森林里深度相同的点的度相同

思路:推几组找规律即可,发现x越大,它可以分成多种前面有的小x,把小x的贡献都加上即可。

1011抓拍

题意:给出n个点,每个点有移动方向,移动方向只可能是上下左右四个方向,然后每个点移动速度是相同的,要求是在任意非负时间上找到一个周长最小的矩形能包括所有的点。

思路:题目要求周长最小的矩形,看起来点很多,我们很难找到怎么围这个矩形,但是稍微思考一下会发现,其实就是找任意时刻上下左右四个跑的最远的点就行,我们可以枚举时间,然后枚举每个点在这个时间对应的位置,找到四个最远的点直接算。但是这样时间复杂度太大了,我们要考虑优化。再仔细读题会发现,每个时间对于任意一个长度的变化只可能是-2 -1 0 1 2其中的一中,既有正数又有负数,那就说明周长曲线应该是一个u行曲线,因此我们不如三分时间,条件就是去逼近最小的中心即可。

1007 单峰数列

题意:给出一个数列以及五种操作,根据对应的操作要求输出即可。

思路:看到段落查询段落修改就应该想到这是一道非常标准的线段树的题目了,问题是改如何维护这个线段树来尽可能的降低时间复杂度,首先直接维护数列是不行的,因为数列很难去维护一段的单调性相同性以及单峰性。思考有什么数据结构可以简单快捷的维护相同,单调。会发现差分数组非常满足这个条件,如果一段差分数组的值都是0,那么说明这一段肯定都是相同的,因为变化量为0,如果一段差分数组的值都是正数,那么一个个累加过去每个位置肯定也是递增的,同理递减也是如此,因此线段树维护一个差分数组就行。而且差分数组还有一个好处(?也可能是坏处,但是写起来是好处,因为更加简单了)就是只需要单点修改就行,因此不需要考虑痛苦的lazy标记,但是因此的复杂度也比较高,不过在这题里面还可以忍受。具体实现比较复杂,请看我的代码,代码里有注释方便理解。

代码

#include<iostream>
#include<string>
#include<cstring>
#include<map>
#include<queue>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
#define ll long long

const ll N = 1e5 + 10;
const ll mod = 998244353;

ll arr[N] = { 0 };
ll brr[N] = { 0 };

struct tree
{
	ll l;
	ll r;
	ll sum; //记录这一段差分数组的和,如果为0说明这一段都无差距即为相等
	ll up; //记录这一段差分数组的状态是上升还是下降,就是判断这一段的值是否都是正数,正数一路加过去肯定是越来越大的
	ll down; //记录这一段差分数组的状态是上升还是下降,如果都是负数那就说明是下降的
}tr[N<<4];

void pushup(ll p) //合并左右子树来确定当前这段的状态
{
	//因为建树的时候差分数组的值是参差不齐的,所以我们在判断时要通过正负去判断,之后我们合并的时候给相等上升和下降统一赋值方便操作
	ll l = p << 1;
	ll r = l + 1;
	if (tr[l].sum == 0 && tr[r].sum == 0) tr[p].sum = 0;  //如果左右子树都是相等的,那就说明这一段都是相等的,因为都是0的话就不会有变化,否则这一段只要有一个值非0那就不会是0
	else tr[p].sum = 1;
	if (tr[l].up > 0 && tr[r].up > 0) tr[p].up = 1; //如果左右段的数都是正数,那这一段肯定也都是正数,那就说明这段是上升的
	else tr[p].up = 0;
	if (tr[l].down < 0 && tr[r].down < 0) tr[p].down = -1; //同理
	else tr[p].down = 0;
	return;
}

void build(ll p, ll l, ll r)
{
	if (l == r)
	{
		tr[p] = { l,l,brr[l],brr[l],brr[l] }; //建树
		return;
	}
	tr[p].l = l;
	tr[p].r = r;
	ll mid = (l + r) / 2;
	build(p * 2, l, mid);
	build(p * 2 + 1, mid + 1, r);
	pushup(p);
	return;
}

void modify(ll p, ll pos, ll v)
{
	if (tr[p].l == pos && tr[p].r == pos)
	{
		tr[p].sum += v;
		tr[p].up += v;
		tr[p].down += v;
		return;
	}
	ll mid = (tr[p].l + tr[p].r) / 2;
	if (pos<=mid)modify(p * 2, pos, v);
	else modify(p * 2 + 1, pos, v);
	pushup(p); //修改完不要忘记合并
	return;
}

ll query(ll p, ll l, ll r, ll op) //查找的时候要根据op的不同值进行不同的划分和返回
{
	if (l <= tr[p].l && r >= tr[p].r)
	{
		if (op == 2) return tr[p].sum;
		else if (op == 3) return tr[p].up;
		else if (op == 4) return tr[p].down;
	}
	ll mid = (tr[p].l + tr[p].r) / 2;
	ll f = 1;
	if (op == 2)
	{
		if (l <= mid) //如果查找范围在当前范围的左子树内,我们就去看左子树的返回值,不用考虑右子树,避免无效返回值影响答案
		{
			if (query(p * 2, l, r, op) != 0) f = 0;
		}
		if (r > mid) //如果查找范围在右子树内也是一样的
		{
			if (query(p * 2 + 1, l, r, op) != 0)f = 0;
		}
		//如果左右子树都各有一部分那就都要查找,所以这边用的是if且没有else,下面都是同理的
		if (f) return 0;
		else return 1;
	}
	else if (op == 3)
	{
		if (l <= mid)
		{
			if (query(p * 2, l, r, op) <= 0) f = 0;
		}
		if (r > mid)
		{
			if (query(p * 2 + 1, l, r, op) <= 0)f = 0;
		}
		if (f) return 1;
		else return 0;
	}
	else
	{
		if (l <= mid)
		{
			if (query(p * 2, l, r, op) >= 0) f = 0;
		}
		if (r > mid)
		{
			if (query(p * 2 + 1, l, r, op) >= 0)f = 0;
		}
		if (f) return -1;
		else return 0;
	}
}


void solve()
{
	ll n;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> arr[i];
		brr[i] = arr[i] - arr[i - 1]; //使用差分数组加线段树做法,brr储存的是差分数组的情况
	}
	build(1, 1, n); //建造线段树
	ll q;
	cin >> q;
	for (int i = 1; i <= q; i++)
	{
		//由于我们用的是差分数组的方式储存一段的变化进行线段树,所以我们进行修改时,要单点修改,对于l和r+1修改就行,如果r+1>n那就不用改了,因为我们一路加到n就是n的值,没必要继续往后加
		//其次就是查找的时候,因为差分数组加上对应位置的值才是变化后该位置的值,所以我们判断一段的状态要从l+1进行查找,因为l作为开始点任何点都是和它对比的,它本身是多少是怎么变化对我们来说无所谓
		ll op,l,r;
		cin >> op >> l >> r;
		if (op == 1)
		{
			ll v;
			cin >> v;
			modify(1, l, v);
			if (r+1<=n)modify(1, r + 1, -v);
		}
		else if (op == 2)
		{
			if (query(1, l+1, r, op) == 0) cout << 1 << endl;
			else cout << 0 << endl;
		}
		else if (op == 3)
		{
			if (query(1, l+1, r, op) > 0 || l == r) cout << 1 << endl;
			else cout << 0 << endl;
		}
		else if (op == 4)
		{
			if (query(1, l+1, r, op) < 0 || l == r)cout << 1 << endl;
			else cout << 0 << endl;
		}
		else
		{
			ll ans = 0;
			ll nl = l+1;
			ll nr = r;
			while (nl <= nr) //先通过二分去查找第一个递增的点到哪里为止,然后找这个点后面是不是递减就行
			{
				ll mid = (nl + nr) / 2;
				if (query(1, nl, mid, 3)) ans=max(ans,mid),nl=mid+1;
				else nr = mid - 1;
			}
			if (query(1, l + 1, ans, 3) <= 0)
			{
				cout << 0 << endl;
				continue;
			}
			if (ans == r)
			{
				cout << 0 << endl;
				continue;
			}
			if (query(1, ans + 1, r, 4) >= 0)
			{
				cout << 0 << endl;
			}
			else
			{
				cout << 1 << endl;
			}
		}
	}
}

signed main()
{
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	ll t;
	t = 1;
	while (t--)
	{
		solve();
	}
}

1008 比特跳跃

题意:给出一个无向图,问从1号城市到每个城市所需要的最少时间,除了按边的要求耗费t的时间从u到v外,还有一个特殊的操作叫比特跳跃,从任意一个x到y,耗费一个k*(x|y)的值。

思路:首先考虑没有特殊操作的情况,发现是很标准的最短路算法,跑一遍djs可以轻松的处理,然后在这个基础上加上特殊操作比特跳跃,发现任意操作非常复杂,但是我们可以根据按位与去化简这个操作,首先就是对于任意的奇数序号点,我们发现奇数序号点之间通过比特跳跃到的最优解是直接从1跳跃到希望点,因为我们从x跳跃到y的代价是先从1正常走到x或者从1跳跃到x再跳跃到y,但是不管选择哪一种,里面至少都有一个k*(x|y)的代价。而x|y的代价肯定>=y|1的值,但是如果我们直接从1跳到目标点,代价肯定是k*(x)比上面两种选择都更优的。那为什么偶数不能这样考虑呢,因为对偶数来说,从1直接跳到偶数结点,会有一个k*(x+1)的代价,而这个+1视k的大小就会导致从x跳跃到y的代价x|y可能更优化,因此我们要考虑x|y就等于两者之间较大值的点之间的跳跃能否有更优解,即二进制下,较小数是较大数的一部分,这样跳跃就是较大数的消耗,不会有那个+1的值,看看这种情况有没有可能更优,优化完后再跑一遍djs即可,因为优化后可能导致某些点的最短路值变小,因此此时还需要再跑一遍djs。

代码:

#include<iostream>
#include<string>
#include<cstring>
#include<map>
#include<queue>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
#define ll long long
#define int long long 
#define PII pair<int,int>
#define x first
#define y second

const ll N = 1e5 + 10;
const ll mod = 998244353;

ll n, m, k;
vector<vector<PII>> edge(n + 1);
ll vis[N] = { 0 };
ll minans[N] = { 0 };
ll ans[N] = { 0 };

ll lowbit(ll a)
{
	return (a & (-a));  //lowbit取二进制第一位1
}

void djs()
{
	priority_queue<PII, vector<PII>, greater<PII>> q; //优先队列q,用于优化djs算法的时间复杂度。
	ans[0] = ans[1] = 0;//从1开始因此1的最短路是0
	q.push({ 0,1 }); //把1推进去依次开始djs
	while (q.size())
	{
		int u = q.top().y;
		q.pop();
		if (vis[u]) continue; //如果这个点已经处理过了那就不再处理
		vis[u] = 1;
		for (int i = 0; i < edge[u].size(); i++)
		{
			int v = edge[u][i].x;
			int val = edge[u][i].y;
			if (ans[v] > ans[u] + val)  //如果从u到v走这条路比之前走到这边的路更小,那就更新这个点的最优解,否则放弃这条边
			{
				ans[v] = ans[u] + val;
				q.push({ ans[v],v }); //这个点更新了后,就要把与这个点有关的点更新,因为这个点的值变小了,与它有关的点的值肯定也会变小,因此把这个点推入队列
			}
		}
	}
	//下面的第二次djs是和上面一样的就不再给注释了
	for (int i = 0; i <= n; ++i) minans[i] = ans[i], vis[i] = 0; //为了进行第二次djs,把vis数组重置,同时记录一下第一次跑完的最优解
	//考虑进行比特跳跃的时候,我们发现,与其随意的跳,不如让每次跳的代价尽可能小,k值是不变的,我们尽量让x|y的值尽可能小。
	//如果我们随便去跳,加的值肯定大于等于x+1,因此我们不如直接跳到1再跳到想要的点,这样的代价肯定是更小的,当然也有例外,那就是跳到偶数的情况,就是他们的x|y的值就是x和y里面较大的那个,二进制的
	//1的位置上两个人是相同的,异或后不会有更大的代价,这种是我们需要额外考虑的。
	for (int i = 2; i <= n; ++i) { 
		if (i != lowbit(i))  //自己是不能跳自己的,如果有这种情况就跳过。
			for (int j = 0; (1 << j) <= n; ++j)
				if (i & (1 << j)) minans[i] = min(minans[i], minans[i ^ (1 << j)]); //如果i的二进制第j位是1,那么跳这位的贡献就是最优的贡献,我们看看最优的值能不能优化
		if (minans[i] + 1ll * k * i < ans[i]) ans[i] = minans[i] + 1ll * k * i;  //因为取用了比特跳跃,所以要更新一下ans加上比特跳跃的值
	}
	for (int i = 1; i <= n; i++) vis[i] = 0, q.push({ ans[i],i }); //再跑一次djs
	while (q.size())
	{
		int u = q.top().y;
		q.pop();
		if (vis[u])continue;
		vis[u] = 1;
		for (int i = 0; i < edge[u].size(); i++)
		{
			int v = edge[u][i].x;
			int val = edge[u][i].y;
			if (ans[v] > ans[u] + val)
			{
				ans[v] = ans[u] + val;
				q.push({ ans[v],v });
			}
		}
	}
	return;
}


void solve()
{
	cin >> n >> m >> k;
	edge = vector<vector<PII>>(n + 1);
	for (int i = 1; i <= m; i++)
	{
		ll a, b, t;
		cin >> a >> b >> t;
		edge[a].push_back({ b,t });
		edge[b].push_back({ a,t });
	}
	for (int i = 1; i <= n; i++)
	{
		vis[i] = 0;
		ans[i] = 1e15;
		if (i != 1)
		{
			edge[1].push_back({ i,k * (i | (ll)1) }); //先连上直接跳的边,这样奇数结点直接从1跳的情况就考虑到了
		}
	}
	djs();
	for (int i = 2; i <= n; i++)  //输出答案即可
	{
		cout << ans[i];
		if (i == n)
		{
			cout << endl;
		}
		else
		{
			cout << ' ';
		}
	}
}

signed main()
{
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	ll t;
	cin >> t;
	while (t--)
	{
		solve();
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值