T1.排水系统
题目描述
对于一个城市来说,排水系统是极其重要的一个部分。
有一天,小 C 拿到了某座城市排水系统的设计图。排水系统由 n n n 个排水结点(它们从 1 ∼ n 1 \sim n 1∼n 编号)和若干个单向排水管道构成。每一个排水结点有若干个管道用于汇集其他排水结点的污水(简称为该结点的汇集管道),也有若干个管道向其他的排水结点排出污水(简称为该结点的排出管道)。
排水系统的结点中有 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
a1≤a2,则上下可以约掉
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(2a2−a1x1+x2)=2a2x32a2−a1x1+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=ABAB…ABC,其中 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=An−1( n ≥ 2 n \ge 2 n≥2 且为正整数)。例如 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[1…len]。
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[1…i],再枚举可能的出现的次数
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[i∗j+1…len]。最后找到所有的
A
=
S
[
1
…
k
]
A=S[1\dots k]
A=S[1…k](
1
≤
k
<
i
1\le k< i
1≤k<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)jmax−jD 的形式。
由于两个
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[1…k]≤i) 且 1 ≤ k ≤ i 1\le k\le i 1≤k≤i 的 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[i−1][j],如果当前有 F ( S [ 1 … i ] ) = j F(S[1\dots i])=j F(S[1…i])=j,则把 c n t [ i ] [ j … 26 ] cnt[i][j\dots 26] cnt[i][j…26] 都加上 1 1 1 就行了。
不难发现,这个数组的前一维可以去掉。进一步的,把 c n t [ j … 26 ] cnt[j\dots 26] cnt[j…26] 这段区间都加上 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 1∼n+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 号柱子上的要求为:
- x x x 号柱子上至少有一个球;
- y y y 号柱子上至多有 m − 1 m - 1 m−1 个球;
- 只能将 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
m−cnt 个球都是
2
2
2。接着我们就把
2
2
2 号柱子上的前
c
n
t
cnt
cnt 个球和
3
3
3 号柱子上的前
m
−
c
n
t
m-cnt
m−cnt 个球按类别移回
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
n≥3 的情况,我们每一轮把同种颜色的球移到一起,问题就转变成颜色总数为
n
−
1
n-1
n−1 的子问题。我们把当前要聚集的颜色看作
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
m−cnt 个编号为零的球移到
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号柱子)+m(2号柱子)−m(最大可能有编号为1的球的个数)=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 1≤ai≤wi( 1 ≤ i ≤ k 1 \le i \le k 1≤i≤k)。
小 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 1≤ci≤k, 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=1∑X(ni+x)j=1,j=c[x]∏klenj−(i−1)∣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(X−1)=(nX+x)j=1,j=c[x]∏klenj−(X−1)∣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;
}
预祝大家来今后的比赛中取得优异的成绩!
谢谢观看!