NOIP 2020题解

本文详细介绍了四道算法题目,包括排水系统的拓扑排序与高精度计算、字符串匹配的优化策略、移球游戏的递归与动态规划思路,以及微信步数问题的数学建模与多项式计算。通过这些题目,读者可以深入了解和掌握相关算法技巧。
摘要由CSDN通过智能技术生成

T1.排水系统

题目描述

对于一个城市来说,排水系统是极其重要的一个部分。

有一天,小 C 拿到了某座城市排水系统的设计图。排水系统由 n n n 个排水结点(它们从 1 ∼ n 1 \sim n 1n 编号)和若干个单向排水管道构成。每一个排水结点有若干个管道用于汇集其他排水结点的污水(简称为该结点的汇集管道),也有若干个管道向其他的排水结点排出污水(简称为该结点的排出管道)。

排水系统的结点中有 m m m 个污水接收口,它们的编号分别为 1 , 2 , … , m 1, 2, \ldots , m 1,2,,m ,污水只能从这些接收口流入排水系统,并且这些结点没有汇集管道。排水系统中还有若干个最终排水口,它们将污水运送到污水处理厂,没有排出管道的结点便可视为一个最终排水口。

现在各个污水接收口分别都接收了 1 1 1 吨污水,污水进入每个结点后,会均等地从当前结点的每一个排出管道流向其他排水结点,而最终排水口将把污水排出系统。

现在小 C 想知道,在该城市的排水系统中,每个最终排水口会排出多少污水。该城市的排水系统设计科学,管道不会形成回路,即不会发生污水形成环流的情况。

思路

STEP1 拓扑排序
“管道不会形成回路”表示这是一张有向无环图 DAG 。于是结合“每一个排水结点有若干个管道用于汇集其他排水结点的污水”这一条件便很容易想到拓扑排序的思路(事实上也可以用搜索的思路)。先把 m m m 个污水接收口的污水量初始化为 1 1 1 ,再在每轮找到一个入度为零的排水节点,把它有的污水平均分配到它所连的其它排水节点内,最后把所有与它相连的结点入度减一就行了。注意入度为零的排水节点不一定是污水接收口!

STEP2 高精
然而这只能拿到 60 60 60 的好成绩,这题需要高精。但注意到约分后分子 p p p 比分母 q q q 往往小得多,分子只需要unsigned long long就行了。注意到每个节点的排出管道不超过五个,不大于五的质数只有 2 2 2 3 3 3 5 5 5 这三个,分母 q q q 便可以以 p = 2 a 3 b 5 c p=2^a3^b5^c p=2a3b5c 的形式存储,输出答案时只需要高精快速幂就行了。

STEP3 分数加法
假设现在有两个分数 p 1 q 1 \frac{p_1}{q_1} q1p1 p 2 q 2 \frac{p_2}{q_2} q2p2 要相加,则有 p 1 q 1 + p 2 q 2 = p 1 q 2 + p 2 q 1 q 1 q 2 \frac{p_1}{q_1}+\frac{p_2}{q_2}=\frac{p_1q_2+p_2q_1}{q_1q_2} q1p1+q2p2=q1q2p1q2+p2q1
然而这样会面临着分子爆unsigned long long的风险,因此需要同时约分。我们考虑所有包含 2 2 2 的引子,则原式可化为 2 a 2 x 1 + 2 a 1 x 2 2 a 1 + a 2 x 3 \frac{2^{a_2}x_1+2^{a_1}x_2}{2^{a_1+a_2}x_3} 2a1+a2x32a2x1+2a1x2 的形式。不妨设 a 1 ≤ a 2 a_1\le a_2 a1a2,则上下可以约掉 2 a 1 2^{a_1} 2a1 ,即化为 原 式 = 2 a 1 ( 2 a 2 − a 1 x 1 + x 2 ) 2 a 1 ( 2 a 2 x 3 ) = 2 a 2 − a 1 x 1 + x 2 2 a 2 x 3 原式=\frac{2^{a_1}(2^{a_2-a_1}x_1+x_2)}{2^{a_1}(2^{a_2}x_3)}=\frac{2^{a_2-a_1}x_1+x_2}{2^{a_2}x_3} =2a1(2a2x3)2a1(2a2a1x1+x2)=2a2x32a2a1x1+x2这样就能够巧妙地解决精度问题。对于 3 3 3 5 5 5 也有类似讨论,这里不再赘述。

代码

#include<iostream>
#include<cstdio>
#define ll unsigned long long
using namespace std;
int n,m,to[100001][6],rd[100001];
int stk[100001],top;
ll pw[6][1001];
ll qpw(ll x,int y){  //记忆化求幂,所有求幂复杂度总共是线性的
	if(pw[x][y]==0) return pw[x][y]=qpw(x,y-1)*x;
	else return pw[x][y];
}
struct num{  //分母
	int a,b,c;
	num(){
		a=b=c=0;
		return;
	}
};
struct big_num{  //高精
	int x[51],len;
	void operator=(ll a){
		len=-1;
		while(a) x[++len]=a%10,a/=10;
		return;
	}
	void write(){
		for(int i=len;i>=0;i-=1) printf("%d",x[i]);
		printf("\n");
		return;
	}
};
big_num operator*(big_num a,big_num b){  //高精乘
	big_num c;
	int z=0,l,r;
	c.len=a.len+b.len;
	for(int i=0;i<=c.len;i+=1){
		c.x[i]=z;
		l=max(0,i-b.len); r=min(i,a.len);
		for(int j=l;j<=r;j+=1){
			c.x[i]+=a.x[j]*b.x[i-j];
		}
		z=c.x[i]/10;
		c.x[i]%=10;
	}
	while(z) c.x[++c.len]=z%10,z/=10;
	return c;
}
struct frac{  //分数
	ll p;
	num q;
	void operator=(ll x){
		p=x;
		return;
	}
	void deal(){  //约分
		while(q.a&&p%2llu==0llu){
			p/=2llu;
			q.a-=1;
		}
		while(q.b&&p%3llu==0llu){
			p/=3llu;
			q.b-=1;
		}
		while(q.c&&p%5llu==0llu){
			p/=5llu;
			q.c-=1;
		}
		return;
	}
	void write(){
		big_num x,y;
		x=qpw(2ll,q.a); y=qpw(3ll,q.b);
		x=x*y; y=qpw(5ll,q.c); x=x*y;
		printf("%llu ",p);
		x.write();
		return;
	}
}f[100001];
frac operator/(frac x,int y){  //均分
	if(y==2) x.q.a+=1;
	if(y==3) x.q.b+=1;
	if(y==4) x.q.a+=2;
	if(y==5) x.q.c+=1;
	x.deal();  //约分
	return x;
}
frac operator+(frac x,frac y){  //分数加
	frac z;
	ll v=x.p,w=y.p;
	z.p=0llu;
	if(x.q.a>=y.q.a) w*=qpw(2llu,x.q.a-y.q.a);
	else v*=qpw(2llu,y.q.a-x.q.a);
	if(x.q.b>=y.q.b) w*=qpw(3llu,x.q.b-y.q.b);
	else v*=qpw(3llu,y.q.b-x.q.b);
	if(x.q.c>=y.q.c) w*=qpw(5llu,x.q.c-y.q.c);
	else v*=qpw(5llu,y.q.c-x.q.c);
	z.p=v+w;
	z.q.a=max(x.q.a,y.q.a);
	z.q.b=max(x.q.b,y.q.b);
	z.q.c=max(x.q.c,y.q.c);
	z.deal();  //约分
	return z;
}
int main(){
//	freopen("water.in","r",stdin);
//	freopen("water.out","w",stdout);
	int x,y;
	pw[2][0]=pw[3][0]=pw[5][0]=1llu;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i+=1){
		if(i<=m) f[i]=1llu;  //初始水量
		else f[i]=0llu;
		scanf("%d",&to[i][0]);
		for(int j=1;j<=to[i][0];j+=1){
			scanf("%d",&to[i][j]);
			rd[to[i][j]]+=1;
		}
	}
	for(int i=1;i<=n;i+=1){
		if(!rd[i]) stk[++top]=i;
	}
	while(top){  //拓扑排序
		x=stk[top--];
		if(to[x][0]) f[x]=f[x]/to[x][0];
		for(int i=1;i<=to[x][0];i+=1){
			y=to[x][i];
			rd[y]-=1;
			if(!rd[y]) stk[++top]=y;
			f[y]=f[x]+f[y];  //累计
		}
	}
	int cnt=0;
	for(int i=1;i<=n;i+=1){
		if(!to[i][0]) f[i].write();  //输出
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T2.字符串匹配

题目描述

小 C 学习完了字符串匹配的相关内容,现在他正在做一道习题。

对于一个字符串 S S S,题目要求他找到 S S S 的所有具有下列形式的拆分方案数:

S = A B C S = ABC S=ABC S = A B A B C S = ABABC S=ABABC S = A B A B … A B C S = ABAB \ldots ABC S=ABABABC,其中 A A A B B B C C C 均是非空字符串,且 A A A 中出现奇数次的字符数量不超过 C C C 中出现奇数次的字符数量。

更具体地,我们可以定义 A B AB AB 表示两个字符串 A A A B B B 相连接,例如 A = aab A = \texttt{aab} A=aab B = ab B = \texttt{ab} B=ab,则 A B = aabab AB = \texttt{aabab} AB=aabab

并递归地定义 A 1 = A A^1=A A1=A A n = A n − 1 A^n = A^{n - 1} An=An1 n ≥ 2 n \ge 2 n2 且为正整数)。例如 A = abb A = \texttt{abb} A=abb,则 A 3 = abbabbabb A^3=\texttt{abbabbabb} A3=abbabbabb

则小 C 的习题是求 S = ( A B ) i C S = {(AB)}^iC S=(AB)iC 的方案数,其中 F ( A ) ≤ F ( C ) F(A) \le F(C) F(A)F(C) F ( S ) F(S) F(S) 表示字符串 S S S 中出现奇数次的字符的数量。两种方案不同当且仅当拆分出的 A A A B B B C C C 中有至少一个字符串不同。

小 C 并不会做这道题,只好向你求助,请你帮帮他。

思路

假设 S S S 下标从 1 1 1 开始,长度为 l e n len len,即 S [ 1 … l e n ] S[1\dots len] S[1len]

STEP1 直接暴力 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn)
这个思路其实非常好想。首先从头枚举 i i i 表示 A B = S [ 1 … i ] AB=S[1\dots i] AB=S[1i],再枚举可能的出现的次数 j j j,这里可以用字符串哈希来判断 A B AB AB 是否循环到 j j j,那么 C = S [ i ∗ j + 1 … l e n ] C=S[i*j+1\dots len] C=S[ij+1len]。最后找到所有的 A = S [ 1 … k ] A=S[1\dots k] A=S[1k] 1 ≤ k < i 1\le k< i 1k<i)满足 F ( A ) ≤ F ( C ) F(A)\le F(C) F(A)F(C) 并累计到答案中就行了。

注意到拆分出来的第一个字符串和最后一个字符串分别是 A A A C C C,便可以提前以 O ( n ) O(n) O(n) 的复杂度处理好 F F F 数组。

STEP2 特殊性质 O ( n ( n + log ⁡ n ) ) O(n(n+\log n)) O(n(n+logn))
若最大次数为 j m a x j_{max} jmax D D D 为剩下的字符串,则 S = ( A B ) j m a x D S=(AB)^{j_{max}}D S=(AB)jmaxD。则 C C C 可以写成 ( A B ) j m a x − j D (AB)^{j_{max}-j}D (AB)jmaxjD 的形式。
由于两个 A B AB AB 拼在一起后所有出现奇数次的字符都被抵消,换句话说, F ( ( A B ) 2 ) = 0 F((AB)^2)=0 F((AB)2)=0

那么就可以得到 F ( D ) = F ( ( A B ) 2 D ) = f ( ( A B ) 4 D ) = … F(D)=F((AB)^2D)=f((AB)^4D)=\dots F(D)=F((AB)2D)=f((AB)4D)= F ( A B D ) = F ( ( A B ) 3 D ) = F ( ( A B ) 5 D ) = … F(ABD)=F((AB)^3D)=F((AB)^5D)=\dots F(ABD)=F((AB)3D)=F((AB)5D)=

因此我们只要统计 C = D C=D C=D C = A B D C=ABD C=ABD 的答案,并分别乘上 F F F 相同的情况数就行了。

STEP3 树状数组 O ( n ( log ⁡ n + log ⁡ 26 ) ) O(n(\log n+\log 26)) O(n(logn+log26))
统计符合条件的 A A A 时,还需要枚举 k k k,这就大大降低了效率。但要求的是 F F F 不大于某个值的前缀字符串个数。

c n t [ i ] [ j ] cnt[i][j] cnt[i][j] 表示枚举到当前位置 i i i 时,满足 F ( S [ 1 … k ] ≤ i ) F(S[1\dots k]\le i) F(S[1k]i) 1 ≤ k ≤ i 1\le k\le i 1ki k k k 的个数。在每一轮时,初始化 c n t [ i ] [ j ] = c n t [ i − 1 ] [ j ] cnt[i][j]=cnt[i-1][j] cnt[i][j]=cnt[i1][j],如果当前有 F ( S [ 1 … i ] ) = j F(S[1\dots i])=j F(S[1i])=j,则把 c n t [ i ] [ j … 26 ] cnt[i][j\dots 26] cnt[i][j26] 都加上 1 1 1 就行了。

不难发现,这个数组的前一维可以去掉。进一步的,把 c n t [ j … 26 ] cnt[j\dots 26] cnt[j26] 这段区间都加上 1 1 1 可以看成把它的差分数组第 j j j 位加上 1 1 1,便可以用树状数组来求前缀和。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#define ull unsigned long long
using namespace std;
ull base=131llu,h[1050000],pow;
char s[1050000];
int t,len,k,tot;
ull ans,cnt[1050001];
int f[1050000],isodd[26];
int lowbit(int x){
	return x&(-x);
}
void update(int x){  //单点加一
	for(x;x<=27;x+=lowbit(x)) cnt[x]+=1;
	return;
}
int sum(int x){  //前缀和
	int res=0;
	for(x;x;x-=lowbit(x)) res+=cnt[x];
	return res;
}
int main(){
//	freopen("string.in","r",stdin);
//	freopen("string.out","w",stdout);
	scanf("%d",&t);
	while(t--){
		ans=0llu;
		scanf("%s",s+1);
		len=strlen(s+1);
		for(int i=0;i<26;i+=1) isodd[i]=cnt[i+1]=0;
		for(int i=1;i<=len+1;i+=1) f[i]=1;  //多组数据初始化
		for(int i=len;i>=1;i-=1){
			isodd[s[i]-'a']^=1;
			f[i]=f[i+1]+(isodd[s[i]-'a']? 1:-1);  //F(S[i...len])
		}
		for(int i=1;i<=len;i+=1) h[i]=h[i-1]*base+s[i]-'a';
		for(int i=0;i<26;i+=1) isodd[i]=0;
		isodd[s[1]-'a']=1;
		update(tot=2);  
		//树状数组下标不能为零,tot表示当前出现奇数次字符个数加一
		pow=base*base;
		for(int i=2;i<=len-1;i+=1){  
			k=1;
			for(int j=i*2;j<=len-1;j+=i){  //求出最大循环次数
				if(h[j]!=h[j-i]*pow+h[i]) break;
				k+=1;
			}
			ans+=(k+1llu)/2llu*sum(f[i*k+1]);  //两种情况
			ans+=k/2llu*sum(f[i*(k-1)+1]);
			isodd[s[i]-'a']^=1;
			tot+=(isodd[s[i]-'a']? 1llu:-1llu);
			update(tot);  //更新cnt
			pow*=base;
		}
		printf("%lld\n",ans);  //输出答案
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T3.移球游戏

题目描述

小 C 正在玩一个移球游戏,他面前有 n + 1 n + 1 n+1 根柱子,柱子从 1 ∼ n + 1 1 \sim n + 1 1n+1 编号,其中 1 1 1 号柱子、 2 2 2 号柱子、……、 n n n 号柱子上各有 m m m 个球,它们自底向上放置在柱子上, n + 1 n + 1 n+1 号柱子上初始时没有球。这 n × m n \times m n×m 个球共有 n n n 种颜色,每种颜色的球各 m m m 个。

初始时一根柱子上的球可能是五颜六色的,而小 C 的任务是将所有同种颜色的球移到同一根柱子上,这是唯一的目标,而每种颜色的球最后放置在哪根柱子则没有限制。

小 C 可以通过若干次操作完成这个目标,一次操作能将一个球从一根柱子移到另一根柱子上。更具体地,将 x x x 号柱子上的球移动到 y y y 号柱子上的要求为:

  1. x x x 号柱子上至少有一个球;
  2. y y y 号柱子上至多有 m − 1 m - 1 m1 个球;
  3. 只能将 x x x 号柱子最上方的球移到 y y y 号柱子的最上方。

小 C 的目标并不难完成,因此他决定给自己加加难度:在完成目标的基础上,使用的操作次数不能超过 820000 820000 820000。换句话说,小 C 需要使用至多 820000 820000 820000 次操作完成目标。

小 C 被难住了,但他相信难不倒你,请你给出一个操作方案完成小 C 的目标。合法的方案可能有多种,你只需要给出任意一种,题目保证一定存在一个合法方案。

思路

这道题只需要找到一种移球的方法,按照它模拟就好了。下面提供其中一种思路。
我们先看到 n = 2 n=2 n=2 的情况。比方说有下面这种情况:
在这里插入图片描述
我们以 c n t cnt cnt 表示 1 1 1 号柱子上颜色为 1 1 1 的球的个数。那么显然有 c n t = 3 cnt=3 cnt=3。于是先把 2 2 2 号柱子上的前 c n t cnt cnt 个球移动到 3 3 3 号柱子上,就得到了下图。
在这里插入图片描述
接着就从上往下依次考虑 1 1 1 号柱子上的球,如果颜色为 1 1 1 就把它移到 2 2 2 号柱子上去;否则移到 3 3 3 号柱子上去。
在这里插入图片描述
于是我们就会发现 2 2 2 号柱子上前 c n t cnt cnt 个球都是 1 1 1 3 3 3 号柱子上前 m − c n t m-cnt mcnt 个球都是 2 2 2。接着我们就把 2 2 2 号柱子上的前 c n t cnt cnt 个球和 3 3 3 号柱子上的前 m − c n t m-cnt mcnt 个球按类别移回 1 1 1 号柱子上,接着把 3 3 3 号柱子剩下的球都移到 2 2 2 号柱子上。
在这里插入图片描述
到这里我们实际上是给 1 1 1 号柱子上球排个序。接着我们把 1 1 1 号柱子上的两种类型的球分开到 1 1 1 号和 3 3 3 号柱子上。
在这里插入图片描述
接下来只要把 2 2 2 号柱子上的球分开就行了。

对于 n ≥ 3 n\ge 3 n3 的情况,我们每一轮把同种颜色的球移到一起,问题就转变成颜色总数为 n − 1 n-1 n1 的子问题。我们把当前要聚集的颜色看作 1 1 1 其它都看作零。于是两步就可以完成每一轮移动。
STEP1 构造全零列
对于下面这种情况,记录 c n t cnt cnt 为当前 1 1 1 号柱子上颜色为 1 1 1 的球的个数,则有 c n t = 2 cnt=2 cnt=2
在这里插入图片描述
类似于上面 n = 2 n=2 n=2 的讨论,我们也可以把 1 1 1 中的 1 1 1 0 0 0 利用 3 3 3 号柱子和 4 4 4 号柱子分开(标红的是原 1 1 1 号柱子上的球):
在这里插入图片描述
接着我们把 4 4 4 号柱子上的 m − c n t m-cnt mcnt 个编号为零的球移到 1 1 1 号柱子上去:
在这里插入图片描述
我们再考虑 2 2 2 号柱子。把其中编号为零的球移到 1 1 1 号柱子上直到填满为止,剩下的球都移动到 4 4 4 号柱子上。
在这里插入图片描述
如图所示(标红的是原 2 2 2 号柱子上的球),第一列就是一个全零列。事实上,由于原来 1 1 1 号和 2 2 2 号柱子上为零的球的个数必定大于 m ( 1 号 柱 子 ) + m ( 2 号 柱 子 ) − m ( 最 大 可 能 有 编 号 为 1 的 球 的 个 数 ) = m m(1号柱子)+m(2号柱子)-m(最大可能有编号为1的球的个数)=m m(1)+m2m1=m,也就是说必能构造全零列。
STEP2 构造全一列
假设上述最后一步之后,把柱子恰当地交换,得到下图:
在这里插入图片描述
记第一列为 1 1 1 的球的个数为 c n t cnt cnt。显然 c n t = 3 cnt=3 cnt=3。类似于构造全零列的操作,我们把 3 3 3 号柱子上的 c n t cnt cnt 个球移到 4 4 4 号柱子上。接着把 1 1 1 号柱子上的 1 1 1 移到 3 3 3 号柱子上,其余移到 4 4 4 号柱子上:
在这里插入图片描述
上图中,红色标记的是原 1 1 1 号柱子上的球。可以发现,此时 4 4 4 号柱子成为新的全零列,而原来 1 1 1 号柱子上的编号为 1 1 1 的球都移到了 3 3 3 号柱子上方,而 3 3 3 号柱子下方全是编号为 0 0 0 的球。适当地交换柱子后,相当于把 1 1 1 号柱子中编号为 1 1 1 的球全部“上移”。可以按照一样的方法,把柱子中的所有编号为 1 1 1 的球“上移”,便得到下图。
在这里插入图片描述
接着我们把编号为 1 1 1 的球全部移到空着的柱子上就得到了全一列,再把原来的全零列上的球全部补到有空位的柱子上。
存储答案时要用到栈,但我们发现相邻的两步未作出任何改变时,就可以把这两步同时消去。

代码

#include<iostream>
#include<cstdio>
using namespace std;
int n,m,ans,ansx[820001],ansy[820001];
int a[52][401],p[52],cnt;
void move(int x,int y){  //单次移动
	a[y][++a[y][0]]=a[x][a[x][0]--];
	if(ansx[ans]==y&&ansy[ans]==x) ans-=1;  //消去
	else ansx[++ans]=x,ansy[ans]=y;
	return;
}
int chk(int x,int y){  //求x号柱子上y的个数
	int res=0;
	for(int i=1;i<=a[x][0];i+=1) res+=(a[x][i]==y);
	return res;
}
int main(){
//	freopen("ball.in","r",stdin);
//	freopen("ball.out","w",stdout);
	scanf("%d%d",&n,&m);
	p[n+1]=n+1;
	for(int i=1;i<=n;i+=1){
		a[i][0]=m; p[i]=i;
		for(int j=1;j<=m;j+=1) scanf("%d",&a[i][j]);
	}
	for(int i=n;i>=3;i-=1){  //n>=3
		cnt=chk(p[1],i);
		for(int j=1;j<=cnt;j+=1){
			move(p[i],p[i+1]);
		}
		for(int j=m;j>=1;j-=1){
			if(a[p[1]][j]==i) move(p[1],p[i]);
			else move(p[1],p[i+1]);
		}
		for(int j=1;j<=m-cnt;j+=1) move(p[i+1],p[1]);
		for(int j=m;j>=1;j-=1){
			if(a[p[2]][j]!=i&&a[p[1]][0]<m) move(p[2],p[1]);
			else move(p[2],p[i+1]);
		}
		swap(p[2],p[i+1]); swap(p[1],p[i]);
		for(int j=1;j<i;j+=1){
			cnt=chk(p[j],i);
			for(int k=1;k<=cnt;k+=1) move(p[i],p[i+1]);
			for(int k=m;k>=1;k-=1){
				if(a[p[j]][k]==i) move(p[j],p[i]);
				else move(p[j],p[i+1]);
			}
			swap(p[j],p[i]); swap(p[i],p[i+1]);
		}
		for(int j=1;j<i;j+=1){
			for(int k=m;k>=1;k-=1){
				if(a[p[j]][k]==i) move(p[j],p[i+1]);
				else break;
			}
			while(a[p[j]][0]<m) move(p[i],p[j]);
		}
	}
	cnt=chk(p[1],1);  //n=2
	for(int i=1;i<=cnt;i+=1) move(p[2],p[3]);
	for(int i=m;i>=1;i-=1){
		if(a[p[1]][i]==1) move(p[1],p[2]);
		else move(p[1],p[3]);
	}
	for(int i=1;i<=cnt;i+=1) move(p[2],p[1]);
	for(int i=1;i<=m-cnt;i+=1) move(p[3],p[1]);
	for(int i=1;i<=cnt;i+=1) move(p[3],p[2]);
	for(int i=1;i<=m-cnt;i+=1) move(p[1],p[3]);
	for(int i=m;i>=1;i-=1){
		if(a[p[2]][i]==1) move(p[2],p[1]);
		else move(p[2],p[3]);
	}
	printf("%d\n",ans);
	for(int i=1;i<=ans;i+=1) printf("%d %d\n",ansx[i],ansy[i]);
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T4.微信步数

题目描述

小 C 喜欢跑步,并且非常喜欢在微信步数排行榜上刷榜,为此他制定了一个刷微信步数的计划。

他来到了一处空旷的场地,处于该场地中的人可以用 k k k 维整数坐标 ( a 1 , a 2 , … , a k ) (a_1, a_2, \ldots , a_k) (a1,a2,,ak) 来表示其位置。场地有大小限制,第 i i i 维的大小为 w i w_i wi ,因此处于场地中的人其坐标应满足 1 ≤ a i ≤ w i 1 \le a_i \le w_i 1aiwi 1 ≤ i ≤ k 1 \le i \le k 1ik)。

小 C 打算在接下来的 P = w 1 × w 2 × ⋯ × w k P = w_1 \times w_2 \times \cdots \times w_k P=w1×w2××wk 天中,每天从场地中一个新的位置出发,开始他的刷步数计划(换句话说,他将会从场地中每个位置都出发一次进行计划)。

他的计划非常简单,每天按照事先规定好的路线行进,每天的路线由 n n n 步移动构成,每一步可以用 c i c_i ci d i d_i di 表示:若他当前位于 ( a 1 , a 2 , … , a c i , … , a k ) (a_1, a_2, \ldots , a_{c_i}, \ldots, a_k) (a1,a2,,aci,,ak),则这一步他将会走到 ( a 1 , a 2 , … , a c i + d i , … , a k ) (a_1, a_2, \ldots , a_{c_i} + d_i, \ldots , a_k) (a1,a2,,aci+di,,ak),其中 1 ≤ c i ≤ k 1 \le c_i \le k 1cik d i ∈ { − 1 , 1 } d_i \in \{-1, 1\} di{1,1}。小 C 将会不断重复这个路线,直到他走出了场地的范围才结束一天的计划。(即走完第 n n n 步后,若小 C 还在场内,他将回到第 1 1 1 步从头再走一遍)。

小 C 对自己的速度非常有自信,所以他并不在意具体耗费的时间,他只想知道 P P P 天之后,他一共刷出了多少步微信步数。请你帮他算一算。

思路

如果发现第一轮后,有些天数的位置并没有发生变动,那么无论经过多少轮它的位置都不变,也就是走不出去的情况。
很容易就想到暴力的做法,也就是按照题目枚举每一天,求出步数,累加到答案。不过这样是能拿到很小一部分分。
考虑在暴力算法上面优化。根据题目,每一天在走出界之前走的路线都相同,因此我们可以对每一步进行所有天数的处理。也就是说,统计在每一步走出界的天数,再将天数乘上步数累加到答案。这样在最坏情况下,一轮过后只在某一维上走了一个位置,那么要走 w w w 轮,每一轮有 n n n 步,每一步还要枚举维度统计答案,这样时间复杂度达到 O ( w k n ) O(wkn) O(wkn),能拿到部分分。
难道就不能优化了吗?我们先把上述优化的方法用数学式子表示出来,在针对式子的特点考虑优化的方法。
第一轮比较特殊,我们单独跑一遍,时间复杂度为 O ( n k ) O(nk) O(nk)。第一轮过后,枚举一轮上的每一步 x x x,先判断是否能从这一步走出界。若能,假设还可以走 X X X 次这一步,目前第 i i i 维上还在界内的区间的长度为 l e n i len_i leni,一轮过后第 i i i 维上位置变化为 v i v_i vi 那么这一步对答案的贡献就是 f ( X ) = ∑ i = 1 X ( n i + x ) ∏ j = 1 , j ≠ c [ x ] k l e n j − ( i − 1 ) ∣ v j ∣ f(X)=\sum_{i=1}^{X}(ni+x)\prod_{j=1,j\not=c[x]}^{k}len_j-(i-1)|v_j| f(X)=i=1X(ni+x)j=1,j=c[x]klenj(i1)vj
如果 g ( X ) = f ( X ) − f ( X − 1 ) = ( n X + x ) ∏ j = 1 , j ≠ c [ x ] k l e n j − ( X − 1 ) ∣ v j ∣ g(X)=f(X)-f(X-1)=(nX+x)\prod_{j=1,j\not=c[x]}^klen_j-(X-1)|v_j| g(X)=f(X)f(X1)=(nX+x)j=1,j=c[x]klenj(X1)vj
观察后发现 g ( X ) g(X) g(X) X X X 的次数为 k k k 。考虑 f f f g g g 构成的序列,不难发现 g g g f f f 的差分序列。对于一个多项式数列,每做一次差分,得到的数列仍是一个多项式数列,次数减少了一。也就是说, f ( X ) f(X) f(X) X X X 的次数为 k + 1 k+1 k+1。注意到 k k k 很小,可以考虑 O ( k 2 ) O(k^2) O(k2) 求出 f ( 1 ) , f ( 2 ) , … , f ( k + 2 ) f(1),f(2),\dots,f(k+2) f(1),f(2),,f(k+2),再用拉格朗日插值法求出 f ( X ) f(X) f(X) 就行了。
优化后时间复杂度为 O ( n k 2 ) O(nk^2) O(nk2),就可以通过了。

代码

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
ll mod=1e9+7,ans,l;
int n,k,w[11],c[500001],d[500001];
int wmin[11],wmax[11],v[11],X,x;
int check(){  //检查是否能够完成
	for(int i=1;i<=k;i+=1){
		if(wmin[i]>wmax[i]) return 0;
		if(v[i]!=0) return 0;
	}
	return 1;
}
void move(int a){  //在某一轮第a步上移动一次
	if(d[a]==1){
		wmin[c[a]]+=1;
		wmax[c[a]]=min(wmax[c[a]]+1,w[c[a]]);
	}
	else{
		wmax[c[a]]-=1;
		wmin[c[a]]=max(wmin[c[a]]-1,1);
	}
}
int abs(int a){
	return a>0? a:(-a);
}
int find(int a){  //目前还能走的次数
	int res=1e9;
	for(int i=1;i<=k;i+=1){
		if(v[i]==0) continue;
		res=min(res,(wmax[i]-wmin[i]+1)/abs(v[i]));
	}
	res+=1;
	for(int i=1;i<=k;i+=1){  //判断是否可以多走一次
		if(v[i]==0) continue;
		if((res-1)*abs(v[i])==wmax[i]-wmin[i]+1){
			res-=1;
			break;
		}
	}
	return res;
}
ll quick_pow(ll a,int b){  //快速幂
	if(b==0) return 1ll;
	if(b==1) return a;
	ll res=quick_pow(a,b/2);
	(res*=res)%=mod;
	if(b&1) (res*=a)%=mod;
	return res;
}
ll inv(ll a){  //乘法逆元
	return quick_pow(a,mod-2);
}
ll sub(ll a,ll b){  //模数下的减法
	if(a<b) return a-b+mod*1ll;
	else return a-b;
}
ll lagrange(int a,int b){
	ll res=0ll,w,y=0ll;
	for(int i=1;i<=k+2;i+=1){  //求出从第2轮到第i+1轮时的总贡献
		l=((i*n*1ll)%mod+b*1ll)%mod;
		for(int j=1;j<=k;j+=1){
			if(j==a) continue;
			(l*=1ll*(wmax[j]-wmin[j]+1-(i-1)*abs(v[j])))%=mod;
		}
		(y+=l)%=mod;
		if(i==X){
			return y;
		}
		l=1ll; w=1ll;  //拉格朗日插值
		for(int j=1;j<=k+2;j+=1){  //注意到j为连续正整数,这里还可以预先处理好阶乘
			if(i==j) continue;
			(l*=(X-j)*1ll)%=mod;
			(w*=sub(i*1ll,j*1ll))%=mod;
		}
		(l*=inv(w))%=mod;
		(l*=y)%=mod;
		(res+=l)%=mod;
	}
	return res;
}
int main(){
//	freopen("walk.in","r",stdin);
//	freopen("walk.out","w",stdout);
	scanf("%d%d",&n,&k);
	for(int i=1;i<=k;i+=1) scanf("%d",&w[i]),wmin[i]=1,wmax[i]=w[i];
	for(int i=1;i<=n;i+=1) scanf("%d%d",&c[i],&d[i]);
	for(int i=1;i<=n;i+=1){
		v[c[i]]+=d[i];
		move(i);
	}
	if(check()) ans=-1ll;  //无法完成
	else{
		for(int i=1;i<=k;i+=1){
			wmin[i]=1; wmax[i]=w[i];
		}
		for(int i=1;i<=2*n;i+=1){
			if(i<=n) x=i; else x=i-n;
			if(d[x]==1&&wmax[c[x]]<w[c[x]]){  //走不出去,没有贡献。下同
				move(x);
				continue;
			}
			if(d[x]==-1&&wmin[c[x]]>1){
				move(x);
				continue;
			}
			if(i<=n){  //第1轮时的贡献
				l=1ll;
				for(int j=1;j<=k;j+=1){
					if(j==c[x]) continue;
					(l*=1ll*(wmax[j]-wmin[j]+1))%=mod;
				}
				(l*=1ll*x)%=mod; (ans+=l)%=mod;
			}
			else{  //第二轮及之后的贡献
				X=find(c[x]);  //求出还可以走的次数
				(ans+=lagrange(c[x],x))%=mod;
			}
			move(x);
			if(wmax[c[x]]<wmin[c[x]]) break;  //全部已经走出界
		}
	}
	printf("%lld\n",ans);
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

预祝大家来今后的比赛中取得优异的成绩!

谢谢观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值