P13238 [GCJ 2015 Finals] Crane Truck 题解

前言

大自然的搬运工

来源自Google Code Jam的官方题解

deepseek得了MVP

欢迎的

指出错误,提供更朴素、更准确的解释,向AI提出疑问

不欢迎的

向我提出疑问

题目

\(2^{40}\) 个位置排成,每个位置初始有一个箱子

有一辆卡车根据一段程序沿环移动并搬运箱子(卡车有无限个箱子无限个位置)

程序指令如下:

  • b:向后移动一个位置
  • f:向前移动一个位置
  • u:在当前位置取走一个箱子
  • d:在当前位置放下一个箱子
  • (:什么都不做
  • ):如果当前位置的箱子数量大于 1,则回到指令序列中最近的 ( 处,并从那里继续执行程序(卡车不会移动)

程序中的 ( 和 ) 指令总是成对出现:一个 ( 之后总会有一个匹配的 )。程序中最多只会有两对这样的括号,并且如果有两对括号,它们不会嵌套——也就是说,可能有以下三种情况:

  • 程序中没有 ( 或 ) 指令;
  • 程序中有一个 (,之后有一个匹配的 );
  • 程序中有一个 (,之后有一个 ),再之后有另一个 (,最后还有一个 )。

神奇的是:

  • 若某位置箱子数目变为 0,则变为 266
  • 若某位置箱子数目变为 277,则变为 1

问:到程序结束时移动多少次(前进次数+后撤次数)

思考

\(step_0\)

请忽略 \(T\)

\(step_1\)

很容易想到这是模拟

那么我们按如下方式实现:

  • 定义一个下标 \(pos\) 并初始化为某个数字,比如 1145141
  • 定义一个数组 \(num[MAXN]\) 并初始化为 1
  • 遍历字符串 \(s\)
  • \(s[i]\)\(b\)\(pos \gets pos-1\)
  • \(s[i]\)\(f\)\(pos \gets pos+1\)
  • \(s[i]\)\(u\)\(num[pos] \gets num[pos]-1\)
  • \(s[i]\)\(d\)\(num[pos] \gets num[pos]+1\)
  • \(s[i]\)\((\),新定义一个 \(w \gets i+1\)
  • \(s[i]\)\()\),分类讨论:若 \(num[pos]>1\),则令 \(pos \gets w\),否则 \(pos \gets pos+1\)

空间:\(O(?)\)
时间:\(O(?)\)
期望得分 0

\(step_2\)

聪明可爱的你马上想到可以把 \(s\) 拆成若干片段,对于每个片段用一组信息描述它执行一次的效果,然后模拟片段的执行

遇到循环时又该怎么办呢?聪明可爱的你又观察到 \(2^{40}\) 个位置以环形排列,所以遇到循环时如果位移很大,就利用环形的周期性跳过大量重复步骤

期望得分 100

等等,满分竟然是 45


代码思路

提示:写的可能不太好,建议结合代码理解,注释是我的主观理解,不一定准确

建议:问deepseek,说的还是比较明白的(蒟蒻还是太废物了QAQ)

0. mod 256处理

因为箱子数量 \(num[i] \in [1,256]\),所以可以转化为 \(num[i] \in [0,255]\)

1. 拆成若干片段

以 ( 和 ) 为分界点,拆成括号外的普通片段和括号内的循环片段(不带括号)

2. 建立执行一次的效果信息

模拟片段,记录以下信息:

  • 移动次数
  • 净位移
  • 对起始位置左侧最远影响距离(\(\in \mathbb{N}\)
  • 对起始位置右侧最远影响距离(\(\in \mathbb{N}\)
  • 发生变化的相对位置(\(\in \mathbb{Z}\))及变化量(\(\in \mathbb{Z}\)

至于相对位置及变化量我们用动态窗口维护并稀疏存图

详见代码

3. 对处理过的程序片段进行模拟

这非常重要:

该过程较难理解(蒟蒻还是太废物了QAQ),建议结合代码并使用AI理解

再次强调注释是我的主观理解,不一定准确


a. 确定“非周期区域”的大小
有关“非周期区域”

环形仓库非常大(\(2^{40}\) 个位置),我们不可能为整个环形分配数组。但是,我们只需要关注那些会影响循环条件判断的位置。

循环条件的判断依据是:当前所在位置的箱子数量是否大于 1

如果某个位置永远不会被循环体的执行所改变,或者它的改变不影响循环是否继续,我们就可以忽略它

需要关心的位置构成了一个有限的连续区间,称为“非周期区域”

对于每个片段:

  • 如果是普通片段位移为 0 的循环片段,只需扩展一次
  • 如果是位移不为 0 的循环片段,应当考虑循环重复和绕环

详见代码

b. 模拟执行

给“非周期区域”创建一个数组,并给卡车初始位置对应一个下标,随后模拟

如果是非循环片段,执行一次后立即退出

如果是循环片段,当卡车当前位置在非周期区域内且该位置的箱子数恰为 1 时,不满足循环条件“箱子数大于 1”,退出循环。

否则继续循环

若卡车走出“非周期区域”(即先前进过,后离开),利用环的周期性挑过大量循环

详见代码

有关复杂度

  • 建立执行一次的效果信息,总需要 \(O(n)\)
  • 对于模拟过程,总体视为 \(O(n^2)\)
  • 空间 \(O(n)\)

code

C++14 (GCC 9) O2

提醒美丽可爱的你:不开 O2 大数据集TLE

怎么能直接复制呢,至少照着代码写一遍哦

点击查看代码

#include<bits/stdc++.h>//美丽可爱大方的万能头文件 

//宏优化
#define u64 uint64_t
#define i64 int64_t
#define u8 uint8_t

using namespace std;

const auto LEN=1ll<<40;//仓库大小

u64 gcd(u64 a,u64 b) { //最大公约数
	while(b) {
		const auto c=b;
		b=a%b;
		a=c;
	}
	return a;
}

u64 lcm(u64 a,u64 b) { //最小公倍数
	return a/gcd(a,b)*b;
}

struct Delta { //存储不含括号的程序片段执行一次的效果

		//istructions 程序片段

	public://公开
		explicit Delta(const string& instructions)//构造函数 将一个字符串转化为delta
			:values(get_delta(instructions)) {
		}//explict 防止隐式类型转换 显式调用构造函数


		u64 count=0;//移动指令次数
		i64 shift=0,left=0,right=0;//净位移shift 最远左影响left(>0) 最远右影响right(>0)
		vector<pair<i64,u8>> values;//pair<相对位置 变化量>


	private://私有
		vector<pair<i64,u8>> get_delta(const string& instructions) {//get_delta 构造并返回变化列表

			deque<u8> q= {0};//滑动窗口各个位置变化量 初始化 相对位置0 变化量0
			auto start=0LL;//窗口左端点

			for(const auto& c:instructions) { //遍历程序片段

				//有关 shift-start
				//deque的下标不能为负数 所以start对应下标0 当前shift就对应shift-start

				if(c=='u')//取走
					--q[shift-start];

				else if(c=='d')//放下
					++q[shift-start];

				else if(c=='b') { //后撤
					++count;
					if(shift==start) {
						//shift超过左端点 所以新建位置
						q.emplace_front(0);
						--start;
					}
					--shift;
				}

				else if(c=='f') { //前进
					++count;
					if(shift-start+1==(i64)q.size()) {
						//shift超过右端点 所以新建位置
						q.emplace_back(0);
					}
					++shift;
				}
			}

			left=-start;
			right=q.size()-left-1;//-1是因为还有位置0

			//2个while 除去无意义的位置 即变化量为0的位置
			while(left>0) {
				if(q[0]!=0)
					break;
				q.pop_front();
				--left;
			}
			while(right>0) {
				if(q.back()!=0)
					break;
				q.pop_back();
				--right;
			}

			vector<pair<i64,u8>>  result;//变化列表

			auto i=-left;//转化为相对位置i
			for(const auto& del:q) { //遍历窗口只存入变化的位置信息 <相对位置 变化量>
				if(del!=0)
					result.emplace_back(i,del);
				++i;
			}

			return result;//返回变化列表
		}
};

u64 simulate(const vector<pair<bool,Delta>>& deltas) { //模拟
	u64 result=0;//移动指令次数
	u64 period=1,left=0,right=0;//所有循环片段公共周期(即所有循环片段位移绝对值的最小公倍数) [left right]非周期区域
	//有关 period
	//用于在计算非周期区域大小时提供额外的缓冲 确保当卡车因为循环而绕环形仓库多圈后 仍能安全地回到非周期区域附近 且所有可能影响循环条件的位置都被覆盖

	//有关 非周期区域
	//如果某个位置永远不会被循环体的执行所改变 或者它的改变不影响循环是否继续 我们就可以忽略它
	//那些需要关心的位置构成了一个有限的连续区间 称为 非周期区域

	for(const auto& kvp:deltas) { //遍历程序片段
		const bool& is_loop=kvp.first;//是不是循环片段
		const Delta& delta=kvp.second;//执行效果

		if(!(is_loop && delta.shift)) { //不是循环片段 或 是循环片段但位移为0
			left+=max(delta.left,delta.shift<0 ? -delta.shift:0);
//		delta.
			//left 最远左影响 需覆盖
			//shift 为负 需覆盖
			//shift 非负 无须在意
			right+=max(delta.right,delta.shift>0 ? delta.shift:0) ;
//		delta.
			//right 最远右影响 需覆盖
			//shift 为正 需覆盖
			//shift 非正 无须在意
		}

		else { //是循环片段且位移不为0
			period=lcm(period,abs(delta.shift));//更新period
			if(delta.shift<0) { //位移向左
				if(delta.left>=-delta.shift)
					//如果成立 说明循环多次执行后 卡车会逐渐向左推进 影响范围可能超出delta.left 需要扩展
					left+=delta.left-(1+delta.left)%-delta.shift;
				left+=period;//保证区域足够大 可以安全地进行批量跳过
				right+=delta.right;//向左位移 所以循环不会使右边界额外增加
			}

			else { //位移向右
				if(delta.right>=delta.shift)
					//如果成立 说明循环多次执行后 卡车会逐渐向右推进 影响范围可能超出delta.right 需要扩展
					right+=delta.right-(1+delta.right)%-delta.shift;
				right+=period;//保证区域足够大 可以安全地进行批量跳过
				left+=delta.left;//向右位移 所以循环不会使左边界额外增加
			}
		}
	}

	i64 curr=left;//因为下标不能为负数 所以位置0对应下标curr -left则对应下标0 right则对应curr+right
	vector<u8> area(left+right+1);//非周期区域各个位置变化量

	//模拟执行
	for(const auto& kvp:deltas) { //遍历程序片段
		const bool& is_loop=kvp.first;//是不是循环片段
		const Delta& delta=kvp.second;//执行效果
		bool visit_area=0;//在该程序片段中是否进入过非周期区域

		while(1) { //重复执行 直至满足条件
			if(!visit_area&&curr>=0&&curr<(i64)area.size())
				//没有进入过非周期区域 当前在非周期区域
				visit_area=1;//标记true
				
			for(const auto& kvp:delta.values)//遍历变化列表
				if(curr+kvp.first>=0&&curr+kvp.first<(i64)area.size())//位置在非周期区域
					area[curr+kvp.first]+=kvp.second;//变化累加
					
			curr+=delta.shift;//位置改变
			result+=delta.count;//累加移动指令次数
			
			if(!is_loop||(0<=curr&&curr<(i64)(area.size())&&area[curr]==0))
				//非循环片段 或 在非周期区域内且该位置为0
				break;
				
			if(visit_area&&!(curr>=0&&curr<(i64)area.size())) { //进入过非周期区域且当前不在非周期区域
				visit_area=0;//重置标记
				i64 rep=0;//需要重复执行当前片段的次数

				if(delta.shift>0) {
					assert(curr>=(i64)area.size());//可删
					//因为不在非周期区域 且 净位移向右 断言 位置在右端点右
					//因为净位移向右 所以只能从右端点离开
					
					const auto target=0-delta.right+LEN;
					//有关 target
					//表示一个参考位置 即在该位置时执行一次循环片段所产生的影响范围不影响非周期区域
					//可以结合位置对应的下标 以及 位置排成环理解
					
					rep=(target-curr-1)/delta.shift+1;//重复执行多少次才能越过target
					//有关-1 +1
					//向上取整
					
					curr+=rep*delta.shift-LEN;//重复执行后的位置
				}

				else {
					assert(curr<0);//可删
					//因为不在非周期区域 且 净位移向左 断言 位置在左端点左
					//因为净位移向左 所以只能从左端点离开
					
					const auto& target=(area.size()-1)+delta.left-LEN;
					//有关 target
					//表示一个参考位置 即在该位置时执行一次循环片段所产生的影响范围不影响非周期区域
					//可以结合位置对应的下标 以及 位置排成环理解
					
					rep=(curr-target-1)/-delta.shift+1;//重复执行多少次才能越过target
					//有关-1 +1
					//向上取整
					
					curr+=rep*delta.shift+LEN;//重复执行后的位置
				}

				result+=rep*delta.count;//一次性累加重复执行的移动指令次数
			}
		}
	}

	return result;
}

u64 solve() {
	string s;
	cin>>s;

	vector<pair<bool,Delta>> deltas;//<是不是循环片段 执行效果>
	u64 i=0,j=s.find('(');

	while(i!=s.size()&&j!=string::npos) {//i未到s末 且 j找到了(  即 还有括号需要处理 且 还有指令
		if(i!=j)//i j之间有指令
			deltas.emplace_back(0,Delta(s.substr(i,j-i)));//(非循环片段 程序片段[i j-1])
			
		i=j+1,j=s.find(')',j+1);//i变为循环片段起点 j向右找)
		
		deltas.emplace_back(1,Delta(s.substr(i,j-i)));//(循环片段 程序片段[i j-1])
		
		i=j+1,j=s.find('(',j+1);//新的开始
	}
	if(i!=s.size())//还有剩余指令
		deltas.emplace_back(0,Delta(s.substr(i)));//(非循环片段 程序片段[i 末尾])
		
	return simulate(deltas);
}

int main() {//主函数 自行理解
	int T;
	cin>>T;

	for(int i=1; i<=T; i++) {
		cout<<"Case #"<<i<<": "
		    <<solve()<<"\n";
	}

	return 0;//好习惯
}


结语

虽然不是我的思路,但还是求赞

posted @ 2026-08-29 11:42  熵澈  阅读(2)  评论(0)    收藏  举报