专栏文章

题解:P3207 [HNOI2010] 物品调度

P3207题解参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@min2wkib
此快照首次捕获于
2025/12/01 19:41
3 个月前
此快照最后确认于
2025/12/01 19:41
3 个月前
查看原文
听说 noip 前写题解可以增加 rp 咕咕?
感觉是道并查集维护连通性的好题。题解采用引导式,因为自己弄了很久才明白,希望尽量解释详细,所以比较长。思路大体上和其他题解差不多,但是补充了更多细节。
xxyy 序列怎么确定?
首先要明确:xix_iyiy_i 不是让你任意选择的,在序列 cc 确定时 xxyy 序列是唯一的。先考虑 yiy_i 尽量小,再考虑 xix_i 尽量小,有空位就占用。
注意到 cic_iyiy_i 固定时,xix_i 每增加 11,最终位置就增加 dd 然后再放到取模意义下。设 ci+yi=xc_i + y_i = x,则可以抽象为 xx 有一条出边 (x+d)modn(x + d) \bmod n。每个 xx 都有一条出边,一条入边,所以这些边形成若干个环。xix_i 增加 11 相当于在环上沿边跳一步。
找空位时先从 (ci+yi)modn(c_i+y_i) \bmod n 所在的环上找,看环上有没有空位,如果没有空位就增加 yiy_i,如果有空位则在环上找到能使 xix_i 的最小的空位。
显然有一种暴力做法是先枚举 yiy_i,再枚举 xix_i。但是这样太慢了。能不能加速呢?
加速
枚举太慢了,因为很多已经被占的位置都会被枚举到。为了跳过这些位置,可以使用并查集维护连通性。这里就是维护一个环上的已被占有的连续段,这样可以在枚举 xix_i 的时候跳过连续段。总共枚举 xix_i 只用了 O(n)O(n) 次。
但是枚举 yiy_i 还是很慢啊?如果环长为 11,时间就被卡飞了。
再加速
已满的环的也可以用并查集维护连续段。具体的,维护的连续段内的任意元素所在的环内都满了。称其为环间并查集。之前那个成为环内并查集。
多个点属于一个环没关系,如果是添加到某一个点的时候,这个环满了,我们在环间并查集标记了这个点,但是这个环里其他点也要在环间并查集标记,这是处理的一个细节。我的程序中采用其他点先不标记,等到后面再处理到这个环时,如果满了但还没有标记,就顺手标记了再往下搜。
到这里,我们确定了 xx 序列和 yy 序列,从而确定了所有盒子的最终位置 pospos。这是第一步,下一步我们要确定怎么移动盒子。
一个错误的想法:每次把目标位置在当前空位的盒子移过去
反例:n=5n=5s=1s=1,编号 1144 的盒子最终位置为 2,0,4,32,0,4,3,你会发现按这种办法则编号 3344 的盒子无法归位。
如何移动
这里有个小 trick,把 00 位置上看做有一个虚盒子(实际不存在),它的目标位置为 ss。盒子移到空位转换为和 00 号盒子交换位置。原问题变成了一个置换问题。
现在每个初始位置和目标位置都能一一对应了。现在把初始位置编号向对应的目标位置编号连一条边,就会发现每个编号入度 11,出度 11,形成了若干个环。注意这一步的环和上一步确定 posipos_i 时的环没有任何关系。不要弄混了。
对于包含 00 节点的环,每次把目标位置在当前空位的盒子移过去,也就是顺着环移动,移动次数为 (环长1)(环长-1)
对于不包含 00 节点的环,因为交换必须借助 00 号箱子,所以先用一次交换把 00 节点换进来,再和上面那种情况一样方法操作即可。最后用一次交换把 00 节点换出去。移动次数为 (环长+1)(环长+1),比上面那种情况多了头尾两次交换。
当然对于自环需要特判。自环不需要任何交换。
完成了,下面是代码。感觉细节挺多。
代码CPP
#include <bits/stdc++.h>
#define int long long
using namespace std;
int fa[100010],fac[100010],c[100010],pos[100010],vis[100010];
int n,s,q,p,m,d;
int root(int x){//环内并查集,维护x位置是否被占
	if(fa[x] == x)return x;
	fa[x] = root(fa[x]);
	return fa[x];
}
int rootc(int x){//环间并查集,维护x所在环是否已满
	if(fac[x] == x)return x;
	fac[x] = rootc(fac[x]);
	return fac[x];
}
void init(){
	for(int i = 0; i <= n; i++)fa[i] = fac[i] = c[i] = pos[i] = vis[i] = 0;
}
void solve(){
	scanf("%lld%lld%lld%lld%lld%lld",&n,&s,&q,&p,&m,&d);
	for(int i = 0; i < n; i++){
		if(i == 0)c[i] = 0;
		else c[i] = (c[i - 1] * q + p) % m;
		pos[i] = -1;
	}
	c[0] = s % n;//反正是第一个考虑,都是空位,直接把它记为s,它肯定会占用s位置
	//但是不能在其他i之前赋值,因为会影响后面的c[i]
	for(int i = 0; i <= n + 5; i++)fa[i] = fac[i] = i;
	for(int i = 0; i < n; i++){
		//j = (c[i] + y) % n,k = (c[i] + yi + xi * d) % n
		int j = c[i] % n;//m不一定小于n,即c[i]不一定小于n,记得取模
		while(rootc(j) != j || root(j) == n){//j所在环已经跑完,直接跳过
			if(rootc(j) == j)fac[j] = rootc((j + 1) % n);
			//这里连边是因为如果j所在的环已经满了,
			//但是最后一个被占的点为环上另一点k,
			//那么k在环间并查集标记了而j没有标记,
			//这里需要补标记
			j = rootc(j);
		}
		int k = j;//x_min=0
		while(root(k) != k)k = root(k);//找到j所在环的空位置
		pos[i] = k;
		if(k != root((k + d) % n)){
			fa[k] = root((k + d) % n);
		}else{
			fac[j] = rootc((j + 1) % n),fa[k] = n;
		//再跳一步就从环上连续段的尾调到头了,说明这个环满了
		}
	}
	//注意概念上置换形成的环与处理pos时每次跳d单位的环不同
	int ans = 0;
	for(int i = 1; i < n; i++){
		if(vis[i] || pos[i] == i)continue;//i所在环已经处理或者自环情况就不用再处理了
		int now = i,fl = 0,cnt = 0;
		while(!vis[now]){
			vis[now] = 1,cnt++;
			if(now == s)fl = 1;
			now = pos[now];
		}
		if(fl)ans += cnt - 1;
		else ans += cnt + 1;
	}
	printf("%lld\n",ans);
}
signed main(){
	int T;
	scanf("%lld",&T);
	while(T--){
		init(),solve();
	}
	return 0;
}

评论

0 条评论,欢迎与作者交流。

正在加载评论...