HFOI 集训记录 20220915 - 测试订正

By DaiRuiChen007\text{By DaiRuiChen007}

题目来源:雀巢咖啡系列模拟赛 #28

I. 升降梯上

题目大意

开启了升降梯的动力之后,探险队员们进入了升降梯运行的那条竖直的隧道,映入眼帘的是一条直通塔顶的轨道、一辆停在轨道底部的电梯、和电梯内一杆控制电梯升降的巨大手柄

Nescafe 之塔一共有 nn 层,升降梯在每层都有一个停靠点。手柄有 mm 个控制槽,第 ii 个控制槽旁边标着一个数 cic_i,满足 n<c1<c2<<cm<n-n<c_1<c_2<\cdots<c_m<ncic_i 表示手柄扳动到该槽时,如果 ci>0c_i>0,电梯将上升 cic_i 层;如果 ci<0c_i<0,表示手柄扳动到该槽时,电梯将下降 ci-c_i 层;并且一定存在一个 ci=0c_i=0 手柄最初就位于此槽中

注意升降梯只能在 1n1\sim n 层间移动,因此扳动到使升降梯移动到 11 层以下、nn 层以上的控制槽是不允许的

电梯每移动一层,需要花费 22 秒钟时间,而手柄从一个控制槽扳到相邻的槽,需要花费 11 秒钟时间,探险队员现在在 11 层,并且想尽快到达 nn 层,他们想知道从 11 层到 nn 层至少 需要多长时间?

数据范围保证 n1000,m20n\le1000,m\le 20

思路分析

把当前所在的位置 pospos 和当前手柄所在的控制槽 optopt 打包成一个状态 (pos,opt)(pos,opt),按题目要求连接边之后再图上跑一边 Dijkstra 即可

时间复杂度 Θ(nm2lognm)\Theta(nm^2\log nm)

代码呈现

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1001,MAXM=21,INF=1e18;
struct node {
	int pos,opt,val;
};
struct status {
	int pos,opt,dis;
	inline friend bool operator <(const status &x,const status &y) {
		return x.dis>y.dis;
	}
};
vector <node> edge[MAXN][MAXM];
int dis[MAXN][MAXM],c[MAXM];
bool vis[MAXN][MAXM];
int n,m;
signed main() {
	int start=-1;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;++i) {
		scanf("%lld",&c[i]);
		if(!c[i]) start=i;
	}
	for(int u=1;u<=n;++u) {
		for(int i=1;i<=m;++i) {
			for(int j=1;j<=m;++j) {
				int v=u+c[j];
				if(v<1||v>n) continue;
				edge[u][i].push_back((node){v,j,2*abs(c[j])+abs(i-j)});
			}
		}
	}
	memset(dis,0x3f,sizeof(dis));
	dis[1][start]=0;
	priority_queue <status> q;
	q.push((status){1,start,0});
	while(!q.empty()) {
		int pos=q.top().pos,opt=q.top().opt;
		q.pop();
		if(vis[pos][opt]) continue;
		vis[pos][opt]=true;
		for(auto e:edge[pos][opt]) {
			int _pos=e.pos,_opt=e.opt;
			if(dis[pos][opt]+e.val<dis[_pos][_opt]) {
				dis[_pos][_opt]=dis[pos][opt]+e.val;
				q.push((status){_pos,_opt,dis[_pos][_opt]});
			}
		}
	}
	int res=INF;
	for(int i=1;i<=m;++i) res=min(res,dis[n][i]);
	if(res==INF) puts("-1");
	else printf("%lld\n",res);
	return 0;
}

II. 塔顶试探

题目大意

探险队员们顺利乘坐升降梯来到了塔顶,塔顶有若干房间,房间之间连有通道,如果把房间看做节点,通道看做边的话,整个塔顶呈现一个有根树结构,并且每个房间的墙壁都涂有若干种颜色的一种

现在探险队员们在树根处,他们打算进一步了解塔顶的结构,为此,他们使用了一种特殊设计的机器人。这种机器人会从队员身边,也就是树根出发,之后对塔顶进行深度优先遍历,机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。最后,机器人会回到树根。显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次),然后,机器人会得到一个颜色序列 SS

但是,探险队员发现这个颜色序列并不能唯一确定塔顶的结构,现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列,由于结果可能会非常大,你只需要输出答案对 10910^9 取模之后的值

数据范围保证:S300|S|\le 300

思路分析

考虑区间 dp,设 dpl,rdp_{l,r} 为区间 [l,r][l,r] 的答案,显然 dpl,r0dp_{l,r}\neq 0 的必要不充分的条件是 Sl=SrS_l=S_r

我们可以考虑枚举 [l,r][l,r] 中的第一颗子树,其搜索完成返回的时候回到的位置 SkS_k,显然 Sl=SkS_l=S_k,此时这一部分的的答案应该是 dpl+1,k1dp_{l+1,k-1},那么剩下的若干棵子树,可以继续看成以当前节点为根的一个子问题,我们可以得到这一部分的方法数是 dpk,rdp_{k,r},总的方法数是两个相乘

状态转移方程如下:

$$dp_{l,r}= \begin{cases} 0 &l>r\\ 1 &S_l=S_r\\ \sum\limits_{k=l+1}^{r-1} [S_k=S_l]\times dp_{l+1,k-1}\times dp_{k,r} &\text{otherwise} \end{cases} $$

时间复杂度 Θ(n3)\Theta(n^3)

思路分析

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=601,MOD=1e9;
int dp[MAXN][MAXN];
char euler[MAXN];
signed main() {
	scanf("%s",euler+1);
	int n=strlen(euler+1);
	for(int i=1;i<=n;++i) dp[i][i]=1;
	for(int len=1;len<n;++len) {
		for(int l=1,r=l+len;r<=n;++l,++r) {
			if(euler[l]!=euler[r]) continue;
			dp[l][r]=dp[l+1][r-1];
			for(int k=l+1;k<=r-1;++k) {
				if(euler[l]!=euler[k]) continue;
				dp[l][r]=(dp[l][r]+dp[l+1][k-1]*dp[k][r]%MOD)%MOD;
			}
		}
	}
	printf("%lld\n",dp[1][n]);
	return 0;
}

III. 礼物运送

题目大意

机器人刚刚探查归来,探险队员们突然发现自己的脚下出现了一朵朵白云,把他们托向了空中。一阵飘飘然的感觉过后,队员们发现自己被传送到了一座空中花园

“远道而来的客人,我们是守护 Nescafe 之塔的精灵。如果你们想拜访护法和圣主的话,就要由我们引路。因此,你们是不是该给我们一点礼物呢?”

队员们往远处一看,发现花园中有 nn 个木箱,mm 条柔软的羊毛小路,每条小路的两个端点都是木箱,并且通过它需要 tit_i 的时间。队员们需要往每个木箱中放一份礼物,不过由于精灵们不想让花园被过多地踩踏,因此运送礼物的任务最多只能由两位探险队员完成。两位探险队员同时从 11 号木箱出发,可以在任意木箱处结束运送,运送完毕时,每只木箱应该被两位队员中至少一人访问过。运送任务所用的时间是两人中较慢的那个人结束运送任务的时间,求这个时间的最小值

数据范围保证:n18n\le 18

思路分析

定义某个人经过的所有点的集合为 S\mathbf S,那么我们可以预处理出所有 f(S)f(\mathbf S) 表示某个人经过了 S\mathbf S 这个点集里的所有点,设全集为 I\mathbf IS\overline{\mathbf S}S\mathbf S 的补集,那么答案为:

$$\text{Answer}=\min_{\mathbf S\in\mathbf I} \left\{\max\left(f(\mathbf S),\min_{\overline{\mathbf S}\subseteq \mathbf T}\left\{f(\mathbf T)\right\}\right)\right\} $$

注意到我们对于每个集合 S\mathbf S 都求出 $g(\mathbf S)=\min\limits_{\mathbf S\in\mathbf T}\{f(\mathbf T)\}$,注意到 g(S)g(\mathbf S) 可以通过枚举每个 T\mathbf T 的时候枚举子集更新做到

最后讲一下如何求出 f(S)f(\mathbf S),显然可以考虑 dp,假设 dpS,udp_{\mathbf S,u} 表示走完了 S\mathbf S 里的所有点,最终停留在 vv 的最小代价,那么对于每条边 (u,v)(u,v) 有转移更新:

$$dp_{\mathbf S\cup \{v\},v}\gets dp_{\mathbf S,u}+w(u,v) $$

注意到可能在 S\mathbf S 内部反复转移,因此转移要多做几次

最终 $f(\mathbf S)=\max\limits_{u\in\mathbf S}\{dp_{\mathbf S,u}\}$

最终的时间复杂度 Θ(3n)\Theta(3^n)

代码呈现

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=18,INF=1e18;
int dp[1<<MAXN][MAXN],f[1<<MAXN],g[1<<MAXN];
inline int bit(int x) { return 1<<x; }
struct node {
	int des,val;
};
vector <node> edge[MAXN];
signed main() {
	int n,m,res=INF;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;++i) {
		int u,v,w;
		scanf("%lld%lld%lld",&u,&v,&w);
		--u,--v;
		edge[u].push_back((node){v,w});
		edge[v].push_back((node){u,w});
	}
	memset(dp,0x3f,sizeof(dp));
	memset(g,0x3f,sizeof(g));
	dp[1][0]=0;
	for(int S=1;S<bit(n);++S) {
		f[S]=INF;
		for(int turns=1;turns<=n;++turns) {
			for(int u=0;u<n;++u) {
				if(dp[S][u]>=INF) continue;
				f[S]=min(f[S],dp[S][u]);
				for(auto e:edge[u]) {
					int v=e.des;
					dp[S|bit(v)][v]=min(dp[S|bit(v)][v],dp[S][u]+e.val);
				}
			}
		}
	}
	for(int S=1;S<bit(n);++S) {
		for(int T=S;T;T=(T-1)&S) {
			g[T]=min(g[T],f[S]);
		}
	}
	for(int S=1;S<bit(n);++S) res=min(res,max(f[S],g[(bit(n)-1)^S]));
	printf("%lld\n",res);
	return 0;
}