这是一个很菜的 Oier 的博客|

Hanx16Msgr

园龄:2年8个月粉丝:12关注:3

2022-08-18 16:18阅读: 37评论: 0推荐: 0

C220818C 城市游历

C220818 城市游历

【题目描述】

Alice 将在城市中旅游 q 天,城市中景点的数目为 n,每一个景点有一个特征值 𝑐𝑖,有 m 条双向道路连接这 n 个景点使得从任意一个景点出发都可以到达其他的景点。

每一条道路都有其困难系数𝑘𝑖,Alice 每天会从起点 x 出发做若干次出行。第 i 天接受程度初始为𝑙𝑖,每次出行结束后会增加 1,直到接受程度大于𝑟𝑖后不再出行。每一次出行Alice 可以经过𝑘𝑖 ≤当前接受程度的道路,Alice 也会去到所有能去的景点。

一次出行的愉悦值为到达的景点种类数,相同特征值的景点视作同一种,一天的愉悦值为当天所有出行的愉悦值之和。请你帮助 Alice 计算第 1 到 q 天每天她所能获得的愉悦值。

【输入格式】

第一行三个整数 n,m,q,x,代表景点数目、道路数目、游历天数与起点。

第二行 n 个整数,代表每一个景点的特征值𝑐𝑖。

接下来 m 行每行两个整数 i,j,k,代表一条连接(i,j),困难系数为 k 的边。

接下来 q 行每行两个整数 l,r,代表这一天的最小与最大接受程度。

【输出格式】

对于每个询问输出一行,代表每天所有出行的愉悦值总和。

【样例输入】(tour.in)

3 2 2 1
1 2 3
1 2 2
1 3 3
1 2
1 3

【样例输出】(tour.out)

3
6

【样例说明】

第一天第一次出行只能到 1,第二次可以到 1,2,愉悦值为 3

第二天第一次可以到 1,第二次可以到 1,2,第三次可以到 1,2,3,愉悦值为 6

【数据范围】

对于 30%的数据,𝑛, 𝑚, 𝑞, 𝑘𝑖, 𝑐𝑖, 𝑙𝑖, 𝑟𝑖 ≤ 100

对于另 20%的数据,𝑚 = 𝑛 − 1

对于另 20%的数据,𝑙𝑖, 𝑟𝑖, 𝑘𝑖 ≤ 105

对于 100%的数据,𝑛, 𝑚 ≤ 5×105, 𝑞 ≤ 105, 1 ≤ 𝑐𝑖 ≤ 600,0 ≤ 𝑘𝑖,𝑙𝑖, 𝑟𝑖 ≤ 109

Solution

膜你赛的 T3,因为忘了把 #define int long long 的注释去掉然后怒丢 40pts警钟敲烂。赛后看了下给的 std,我的做法貌似还爆标了,所以就来写篇博客纪念下。

看到数据范围,图是 5e5 这一级别的,并且还有 1e5 的询问,询问的值域到了 1e9,因为并不好想到一些在图上的倍增一类的 log 级的算法,所以我就想的是预处理出答案然后询问的时候直接输出,于是开始思考如何记录答案。

观察样例,不难发现询问的区间 [l,r] 的答案是具有前缀和性质的,这也就是说可以预处理出 [1,l1][1,r] 两个区间的答案然后相减得到。但是因为值域是在 1e9 这一级别,所以肯定是不能直接开一个数组 sumx 来存 [1,x] 的答案的,观察到题目中的 n5e5 的,也就是说 k 的取值只有 5e5 种,用 vecx 来表示在起点接受程度为 x 时可以到达的最多点数,那么 vecx 的值一定也是与 k 相关的,因此我们只记录这 5e5 个值(可以理解做离散化),并在询问时根据 vecx 来推算答案。

因为每一次都是从起点出发,然后每次出行的接受程度 curk 都是固定的,所以预处理的时候可以扫一遍图,每次选择 k 值最小的边进行拓展,直到将整张图扫完(用一个小根堆实现),具体方法参考代码进行讲解:

void prework()
{
	int cnt[605],totans=1,curk=0;mem(cnt,0);
	//cnt是桶,用于统计走过的不同点,totans内存储走过的不同点数,curk表示要到达当前点最少需要的接受程度
	cnt[c[s]]=1;vis[s]=1;//无论如何起点都是可以到达的,所以将起点加入贡献
	queue<int> q;//搜索队列
	q.push(s);//加入起点
	vec.push_back(make_pair(0,1));//表示当接受程度为0的时候可以走到一个不同的点
	while (!eq.empty() || !q.empty())//eq是用来记录边的小根堆,两个任意有一个非空就需要进行搜索
	{
		if (!q.empty())
		{
			int x=q.front();q.pop();//取出队头
			for (int i=head[x];i;i=edge[i].nxt)
			{
				if (vis[edge[i].to]) continue;//如果前进的点已经去过了就不需要再次去了
				eq.push(make_pair(-edge[i].len,i));//priority_queue默认大根堆,将k值取负数就成了小根堆
			}
		}
		if (!eq.empty())//如果还有没拓展的边
		{
			int curedge=eq.top().second;//取出当前k值最小的边的编号
			eq.pop();
			curk=max(curk,edge[curedge].len);//如果要经过这条边到达另一端至少需要有的接受程度,以此更新curk
			if (!cnt[c[edge[curedge].to]]) totans++;//如果这个新的点的类型不同,累加进totans内
			cnt[c[edge[curedge].to]]=1;//标记
			vec.push_back(make_pair(curk,totans));//接受程度为curk的时候的答案为totans,具体为什么不需要去重,在后面询问的时候会处理的
			if (!vis[edge[curedge].to])//如果新的点还没去过就加入搜索队列
				q.push(edge[curedge].to),vis[edge[curedge].to]=1;
		}
	}
}

关于时间复杂度,很明显,因为每个点最多进队出队一次,每个边进队出队一次,所以预处理的时间复杂度是 O(n+mlogm) 的。并且由于 curk 每次都是做的取 max 的运算,因此 vec.first 是单调不减的。
PS. 其实这好像就是用 Prim 算法跑了个最小生成树,但是我在膜你赛的时候完全都没往最小生成树方面去想(

继续看题,会发现只求出一个 vec 数组其实是不够用的,因为询问的内容显然是前缀和一样的东西,所以我们将 vec 数组的前缀和储存进一个 sum 中。此时求前缀和的操作是非常特殊的,因为编号是不一致的,所以需要用到特殊的方法进行处理。首先 sumx.first 的第一维上显然就是 vecx.first,接受程度肯定是不发生改变的,对于 second 的前缀和的求解则需要与 first 的值挂钩。

假设有一个接受程度 t,而这个 t 并不是 vec 中所存有的任意特殊值,假设 vec.first 中最接近 t 且小于 t 的值是 val,那么因为 t>val 并且 t 没有更多的接受程度来经过更多的点,因此接受程度为 t 时的答案一定也是接受程度为 val 时的答案,也就是说 vec 中存储的两个特殊点之间的所有值都是相同的。据此可以推导出 sumx.second 的求解方式(下面将 first 简写做 Fsecond 简写做 S):

sumx.S=sumx1.S+vecx1.S×(vecx.Fvecx1.F1)+vecx.S

解释一下这个式子的含义,前缀和的求解仍然建立在递推的基础上,相邻两个特殊的 F 值之间没有计算到的 vec 的个数为 vecx.Fvecx1.F1(因为 vecx1.S 已经被计算在了 sumx1.S 中,并且上一个 vec 的值并不包含当前 x 这个),然后因为是要计算前缀和,所以要将当前这个数的 vecx.S 值加上。这样就求解出了 vec 的前缀和数组 sum。之后询问的时候就可以利用这个式子进行一些变形就可以计算了。

然后就是询问部分。刚才提到了 vec.first 具有单调性,因此 sum.first 也具有单调性。所以当询问 [1,l] 的时候,可以在 sum.first 中二分到最靠右的那个小于等于 l 的位置,然后利用求前缀和相同的方式求出答案,具体还是结合代码讲解:

int Get(int x)//这个函数的作用就是查找到最靠右的那个小于等于x的位置
{
	int l=0,r=sum.size()-1;//定义左右边界
	while (l<=r)
	{
		int mid=l+r>>1;
		if (sum[mid].first<=x) l=mid+1;
		else r=mid-1;//需要知道,进入此部分分支的时候mid的值一定是在x右侧的,当循环结束的时候,mid将会停留在最靠左的大于x的位置上,此时r=mid-1,就是恰好最靠右的那个小于等于x的位置
	}
	return sum[r].second+(x-sum[r].first)*vec[r].second;//先是获得前r的前缀和,然后[r,x]之间的数之和就是这部分的vec值乘上这部分值的个数
}

这时候就可以来说说为什么不用去重了。我们二分到的最终位置是最靠右的小于等于 x 的位置,因为要最靠右,所以左边一系列的重复的值都会被舍弃掉,只保留最右边那个,因此正确性是可以保证的(其实如果去重的话代码效率可能还会有提升,只不过提升多少随数据而定)。

询问要求区间 [l,r] 的答案,所以答案就是 [1,r][1,l1]

询问部分时间复杂度是 O(qlogm) 的(因为有 m 条边所以 sum 的长度就是 m)。

综合来看,这种解法的时间复杂度是与 c 的值域无关的,为 O(nlog) 这一级别,而 std 的时间复杂度是 O(qmax{ci}) 的,不过因为这道题 c 的值域只有 600,所以两种做法的耗时还是差不多的(而且我人傻常数大,说不定还跑得更慢),不过如果加大 c 的值域,那么 std 的做法就没法胜任了,而对本篇题解的做法的影响,只有在预处理的时候 cnt 数组该开到函数外还是函数内的区别了。

Code

最主要的两个函数 preworkGet 如果明白了那主函数里面应该都不需要要提了吧(

#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
template<typename T> void write(T k) {if (k<0) {putchar('-'),write(-k);return;}if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
vector<pair<int,int> > vec,sum;//f[i]=j -> pair,sum of vec
priority_queue<pair<int,int> > eq;//edge to go,first is len of edge,second is num of edge
const int _SIZE=5e5;
struct EDGE{
	int nxt,to,len;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5];
void AddEdge(int x,int y,int len)
{
	edge[++tot]=(EDGE){head[x],y,len};
	head[x]=tot;
}
int n,m,q,s,c[_SIZE+5];
bool vis[_SIZE+5];
void prework()
{
	int cnt[605],totans=1,curk=0;mem(cnt,0);
	//cnt是桶,用于统计走过的不同点,totans内存储走过的不同点数,curk表示要到达当前点最少需要的接受程度
	cnt[c[s]]=1;vis[s]=1;//无论如何起点都是可以到达的,所以将起点加入贡献
	queue<int> q;//搜索队列
	q.push(s);//加入起点
	vec.push_back(make_pair(0,1));//表示当接受程度为0的时候可以走到一个不同的点
	while (!eq.empty() || !q.empty())//eq是用来记录边的小根堆,两个任意有一个非空就需要进行搜索
	{
		if (!q.empty())
		{
			int x=q.front();q.pop();//取出队头
			for (int i=head[x];i;i=edge[i].nxt)
			{
				if (vis[edge[i].to]) continue;//如果前进的点已经去过了就不需要再次去了
				eq.push(make_pair(-edge[i].len,i));//priority_queue默认大根堆,将k值取负数就成了小根堆
			}
		}
		if (!eq.empty())//如果还有没拓展的边
		{
			int curedge=eq.top().second;//取出当前k值最小的边的编号
			eq.pop();
			curk=max(curk,edge[curedge].len);//如果要经过这条边到达另一端至少需要有的接受程度,以此更新curk
			if (!cnt[c[edge[curedge].to]]) totans++;//如果这个新的点的类型不同,累加进totans内
			cnt[c[edge[curedge].to]]=1;//标记
			vec.push_back(make_pair(curk,totans));//接受程度为curk的时候的答案为totans,具体为什么不需要去重,在后面询问的时候会处理的
			if (!vis[edge[curedge].to])//如果新的点还没去过就加入搜索队列
				q.push(edge[curedge].to),vis[edge[curedge].to]=1;
		}
	}
}
int Get(int x)//这个函数的作用就是查找到最靠右的那个小于等于x的位置
{
	int l=0,r=sum.size()-1;//定义左右边界
	while (l<=r)
	{
		int mid=l+r>>1;
		if (sum[mid].first<=x) l=mid+1;
		else r=mid-1;//需要知道,进入此部分分支的时候mid的值一定是在x右侧的,当循环结束的时候,mid将会停留在最靠左的大于x的位置上,此时r=mid-1,就是恰好最靠右的那个小于等于x的位置
	}
	return sum[r].second+(x-sum[r].first)*vec[r].second;//先是获得前r的前缀和,然后[r,x]之间的数之和就是这部分的vec值乘上这部分值的个数
}
signed main()
{
	read(n),read(m),read(q),read(s);
	for (int i=1;i<=n;i++) read(c[i]);
	for (int i=1;i<=m;i++)
	{
		int u,v,c;read(u),read(v),read(c);
		AddEdge(u,v,c);AddEdge(v,u,c);
	}
	prework();
	sum.push_back(vec[0]);
	for (int i=1;i<vec.size();i++) sum.push_back(make_pair(vec[i].first,sum[i-1].second+vec[i].second+vec[i-1].second*(vec[i].first-vec[i-1].first-1)));
	for (int i=1;i<=q;i++)
	{
		int l,r;read(l),read(r);
		int ans1=Get(l-1);
		int ans2=Get(r);
		writewith(ans2-ans1,'\n');
	}
	return 0;
}

数组版:

#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
template<typename T> void write(T k) {if (k<0) {putchar('-'),write(-k);return;}if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
const int _SIZE=5e5;
pair<int,int> vec[_SIZE+5],sum[_SIZE+5];//f[i]=j -> pair,sum of vec
int tv=0,ts=0;
priority_queue<pair<int,int> > eq;//edge to go,first is len of edge,second is num of edge
struct EDGE{
	int nxt,to,len;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5];
void AddEdge(int x,int y,int len)
{
	edge[++tot]=(EDGE){head[x],y,len};
	head[x]=tot;
}
int n,m,q,s,c[_SIZE+5];
bool vis[_SIZE+5];
void prework()
{
	int cnt[605],totans=1,curk=0;mem(cnt,0);
	cnt[c[s]]=1;vis[s]=1;
	queue<int> q;
	q.push(s);
	vec[++tv]=make_pair(0,1);
	while (!eq.empty() || !q.empty())
	{
		if (!q.empty())
		{
			int x=q.front();q.pop();
			for (int i=head[x];i;i=edge[i].nxt)
			{
				if (vis[edge[i].to]) continue;
				eq.push(make_pair(-edge[i].len,i));
			}
		}
		if (!eq.empty())
		{
			int curedge=eq.top().second;
			eq.pop();
			curk=max(curk,edge[curedge].len);
			if (!cnt[c[edge[curedge].to]]) totans++;
			cnt[c[edge[curedge].to]]=1;
			vec[++tv]=make_pair(curk,totans);
			if (!vis[edge[curedge].to])
				q.push(edge[curedge].to),vis[edge[curedge].to]=1;
		}
	}
}
int Get(int x)
{
	int l=1,r=ts;
	while (l<=r)
	{
		int mid=l+r>>1;
		if (sum[mid].first<=x) l=mid+1;
		else r=mid-1;
	}
	return sum[r].second+(x-sum[r].first)*vec[r].second;
}
signed main()
{
	read(n),read(m),read(q),read(s);
	for (int i=1;i<=n;i++) read(c[i]);
	for (int i=1;i<=m;i++)
	{
		int u,v,c;read(u),read(v),read(c);
		AddEdge(u,v,c);AddEdge(v,u,c);
	}
	prework();
	sum[++ts]=vec[1];
	for (int i=2;i<=tv;i++) sum[++ts]=make_pair(vec[i].first,sum[i-1].second+vec[i].second+vec[i-1].second*(vec[i].first-vec[i-1].first-1));
	for (int i=1;i<=q;i++)
	{
		int l,r;read(l),read(r);
		int ans1=Get(l-1);
		int ans2=Get(r);
		writewith(ans2-ans1,'\n');
	}
	return 0;
}

代码实现的时候遇到的一些问题: 在预处理的时候需要取出 eq 中的边的时候记得判断 eq 是否非空,我在第一次写这部分的时候没有判断,结果发现 eq.size() 因为是 unsigned int 类型,直接溢出飞到 2321 去了,然后一看内存占用直接飙升到 98%(还好关的快,不然绝对死机)。

又在想该放到什么标签下面了……

posted @   Hanx16Msgr  阅读(37)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起