二〇一九年二月五日本文发布
二〇二二年三月二十九日增补了 H 题题解
二〇二三年五月二十八日增补了 F 题题解
二〇二三年一十一月四日增补了 L 题题解
Prologue
时间:20181222-12:20:00 to 20181222-17:20:00
现场比赛(大一限定)时 L 题题目表述有误(但修正后仍无 AC)。现场无 AC 的题有 F,H,I,L(I 题现场无提交)。另有 C 题仅 3 人 AC,D 题仅 1 人 AC(均为现场,其中 C 题可能原因为题面表述不明)。
A
题目大意:给两个长度均为 N 数组 L 1.. n L_{1 .. n} L1..n 和 G 1.. n G_{1 .. n} G1..n,正反序比对,是否相应位置元素均有 L i ≤ G i L_i ≤ G_i Li≤Gi。
水题。代码如下:
#include<stdio.h>
#define MAX_N (100005)
int l[MAX_N];
int main()
{
bool b,f;
int g,n,t;
scanf("%d",&t);
while(t--)
{
b=f=true;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&l[i]);
for(int i=0;i<n;i++)
scanf("%d",&g),
f&=l[i]<=g,b&=l[n-i-1]<=g;
puts(f?b?"both":"front":b?"back":"none");
}
return 0;
}
B
题目大意:给长度为 n 正整数列和正整数 m,判断数列中是否存在区间和为 m 倍数。
计算出前缀和数组,判断数组中是否有对 m 模相同余数的项即可。代码如下:
#include<stdio.h>
#define MAX_M (2005)
bool a[MAX_M];
int main()
{
bool flag;
int m,n,x,s;
while(scanf("%d %d",&n,&m)==2)
{
for(int i=0;i<m;i++)
a[i]=false;
flag=false,s=0;
while(n--)
scanf("%d",&x),s=(s+x)%m,flag|=a[s],a[s]=true;
puts(flag?"YES":"NO");
}
return 0;
}
C
题目大意:给长度为 n 正整数列 a 1.. n a_{1 .. n} a1..n,建图如下:若 g c d ( a i , a j ) > 1 gcd(a_i, a_j) > 1 gcd(ai,aj)>1,则于点 a i a_i ai 和点 a j a_j aj 间存在一条无向边;若 a i = a j a_i = a_j ai=aj,则为同一点。求连通块数和最大连通块大小。
由于每个连通块都存在公共质因子,处理出数列中每个数的所有质因子,并查集合并点集即可。代码如下:
#include<stdio.h>
#define MAX_N (100005)
int sp[MAX_N],st[MAX_N],sz[MAX_N];
int fnd(int x)
{
if(st[x]==x)
return x;
return st[x]=fnd(st[x]);
}
int main()
{
int ai,aj,n;
while(scanf("%d",&n)==1)
{
for(int i=0;i<MAX_N;i++)
sp[i]=-1,st[i]=i,sz[i]=0;
while(n--)
{
scanf("%d",&ai);
if(sz[fnd(ai)]>0)
continue;
aj=ai,sz[ai]=1;
for(int i=2;i*i<=ai&&aj>1;i++)
if(aj%i==0)
{
if(sp[i]!=-1)
{
sp[i]=fnd(sp[i]),st[sp[i]]=ai;
if(sp[i]!=ai)
sz[ai]+=sz[sp[i]],sz[sp[i]]=0;
}
else
sp[i]=ai;
for(;aj%i==0;aj/=i);
}
if(aj>1)
if(sp[aj]!=-1)
{
sp[aj]=fnd(sp[aj]),st[sp[aj]]=ai;
if(sp[aj]!=ai)
sz[ai]+=sz[sp[aj]],sz[sp[aj]]=0;
}
else
sp[aj]=ai;
}
ai=aj=0;
for(int i=0;i<MAX_N;i++)
if(sz[i]>0)
{
if(aj<sz[i])
aj=sz[i];
ai++,sz[i]=0;
}
printf("%d %d\n",ai,aj);
}
return 0;
}
D
题目大意:给 N 个点无根树,边权为 1。M 次操作,设所有点初始均为基态,每次操作两个点 a 和 b,若为基态则变为激发态,若为激发态则变为基态。操作后,激发态点数必为偶,则存在一种方案使所有激发态点两两配对,并使所有激发态点对间距离和最小。求每次操作后最小距离和。
显然,最小点对距离和方案必然路径不相交,即所有边最多通过一次(点可能多次)。若存在一种方案存在 A-C-D-F 和 B-C-D-E 两条路径(即激发态点 ABEF,边集 C-D 通过两次),则另一种方案 A-C-B 和 E-D-F 显然更优。归纳出一般规律,对于任一种方案,若某边通过偶数次则最优方案通过 0 次,若某边通过奇数次则最优方案通过 1 次。对于一次操作,相当于点 a 和点 b 间加上一条路径(由于树性质,路径唯一)。当经过一个点的路径数为偶时,该点为基态(上例中点 C 有 A-C 一、B-C 一、C-D 二、共四为偶,故为基态),否则为激发态。每次查询所有边中经过次数为奇的边数即为最小点对距离和。
可以看出,每次操作只改变一条路径上边的奇偶性。将每次操作看成查询路径 a-b 上所有边权和,并将路径 a-b 上所有边权乘以 -1,上次查询结果加上本次查询路径边权和即为本次查询结果。这样,每次操作后,边权为 -1 的边为参与点对匹配的边,边权为 1 的边为不参与点对匹配的边。如此,问题转化成维护一棵初始边权均为 1 的树,操作类型有区间乘 -1 和区间和查询,数链剖分套线段树即可。代码如下:
#include<stdio.h>
#define MAX_N (100005)
#define MAX_2LGN (0x3FFFF)
int vdg[MAX_N];
struct E
{
int to;
E *nxt;
}edg[MAX_N+MAX_N],*fdg[MAX_N];
struct V
{
int dep,fah,gfa,siz,son;
}vtx[MAX_N];
struct SegTreeNode
{
int bl,br,fg,ky;
}stn[MAX_2LGN];
void dfs0(int n)
{
vtx[n].siz=1,vtx[n].son=0;
for(E *i=fdg[n];i!=NULL;i=i->nxt)
if(i->to!=vtx[n].fah)
{
vtx[i->to].dep=vtx[n].dep+1;
vtx[i->to].fah=n;
dfs0(i->to);
vtx[n].siz+=vtx[i->to].siz;
if(vtx[vtx[n].son].siz<vtx[i->to].siz)
vtx[vtx[n].son].gfa=vtx[n].son,
vtx[n].son=i->to;
else
vtx[i->to].gfa=i->to;
}
}
void dfs1(int n)
{
for(E *i=fdg[n];i!=NULL;i=i->nxt)
if(i->to!=vtx[n].fah&&i->to!=vtx[n].son)
dfs1(i->to);
if(vtx[n].son!=0)
vtx[vtx[n].son].gfa=vtx[n].gfa,
dfs1(vtx[n].son);
vdg[n]=++vdg[0];
}
void SegTreeBuild(int n,int l,int r)
{
stn[n].bl=l,stn[n].br=r,stn[n].fg=1;
if(l+1==r)
{
stn[n].ky=1;
return;
}
int m=(l+r+1)/2;
SegTreeBuild(n*2,l,m);
SegTreeBuild(n*2+1,m,r);
stn[n].ky=stn[n*2].ky+stn[n*2+1].ky;
}
#define SegTreeFlag(x) \
if(stn[x].fg==-1) \
{ \
stn[x].fg=-stn[x].fg; \
stn[x].ky=-stn[x].ky; \
if(stn[x].bl+1<stn[x].br) \
stn[(x)*2].fg=-stn[(x)*2].fg, \
stn[(x)*2+1].fg=-stn[(x)*2+1].fg; \
}
void SegTreeModify(int n,int ql,int qr)
{
if(stn[n].bl>=ql&&stn[n].br<=qr)
{
stn[n].fg=-stn[n].fg;
SegTreeFlag(n);
return;
}
SegTreeFlag(n);
if(stn[n*2].br>ql)
SegTreeModify(n*2,ql,qr);
else
SegTreeFlag(n*2);
if(stn[n*2+1].bl<qr)
SegTreeModify(n*2+1,ql,qr);
else
SegTreeFlag(n*2+1);
stn[n].ky=stn[n*2].ky+stn[n*2+1].ky;
}
int SegTreeQuery(int n,int ql,int qr)
{
SegTreeFlag(n);
if(stn[n].bl>=ql&&stn[n].br<=qr)
return stn[n].ky;
int s=0;
if(stn[n*2].br>ql)
s+=SegTreeQuery(n*2,ql,qr);
else
SegTreeFlag(n*2);
if(stn[n*2+1].bl<qr)
s+=SegTreeQuery(n*2+1,ql,qr);
else
SegTreeFlag(n*2+1);
stn[n].ky=stn[n*2].ky+stn[n*2+1].ky;
return s;
}
int lca(int u,int v)
{
int s=0;
if(vtx[u].gfa!=vtx[v].gfa)
{
if(vtx[vtx[u].gfa].dep<vtx[vtx[v].gfa].dep)
u^=v^=u^=v;
s=SegTreeQuery(1,vdg[u],vdg[vtx[u].gfa]+1)+lca(vtx[vtx[u].gfa].fah,v);
SegTreeModify(1,vdg[u],vdg[vtx[u].gfa]+1);
}
else
{
if(vtx[u].dep<vtx[v].dep)
u^=v^=u^=v;
if(u!=v)
s=SegTreeQuery(1,vdg[u],vdg[vtx[v].son]+1),
SegTreeModify(1,vdg[u],vdg[vtx[v].son]+1);
}
return s;
}
int main()
{
int a,b,m,n,t;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
fdg[i]=NULL;
for(int i=1;i<n;i++)
scanf("%d %d",&edg[i+n].to,&edg[i].to),
edg[i+n].nxt=fdg[edg[i].to],fdg[edg[i].to]=&edg[i+n],
edg[i].nxt=fdg[edg[i+n].to],fdg[edg[i+n].to]=&edg[i];
vtx[0].siz=vtx[1].fah=0,vtx[1].dep=1,dfs0(1);
vdg[0]=0,vtx[1].gfa=1,dfs1(1);
SegTreeBuild(1,1,n);
scanf("%d",&m);
n=0;
while(m--)
scanf("%d %d",&a,&b),
printf("%d\n",n+=lca(a,b));
}
return 0;
}
E
题目大意:给 n 个区间,判断是否存在相交区间。
水题。代码如下:
#include<algorithm>
#include<stdio.h>
#define MAX_N (100005)
struct POINT
{
int x,y;
}itv[MAX_N+MAX_N];
bool cmp(POINT p,POINT q)
{
return p.x<q.x;
}
int stk[MAX_N];
int main()
{
bool flag;
int n;
while(scanf("%d",&n)==1)
{
for(int i=0;i<n;i++)
scanf("%d %d",&itv[i].x,&itv[i+n].x),itv[i].y=i+1,itv[i+n].y=-i-1;
std::sort(itv,itv+n+n,cmp);
flag=true,stk[0]=0;
for(int i=0;i<n+n&&flag;i++)
if(itv[i].y>0)
stk[++stk[0]]=itv[i].y;
else if(-itv[i].y==stk[stk[0]])
--stk[0];
else
flag=false;
puts(flag?"NO":"YES");
}
return 0;
}
F
题目大意:设方程 a 1 x 1 + a 2 x 2 + . . . . + a n x n − d y = 0 a_1x_1 + a_2x_2 + .... + a_nx_n - dy = 0 a1x1+a2x2+....+anxn−dy=0 求最小正整数 y 使得该方程有非负整数解。d ≤ 40,000,n ≤ 100,1 ≤ a i a_i ai ≤ 2 ∗ 1 0 9 2*10^9 2∗109。
图论。建 d 个点 dn 条边的有向图,设顶点集 { v 0 , v 1 , . . . , v d − 1 } \{v_0, v_1, ..., v_{d - 1}\} {v0,v1,...,vd−1}。对于 a i a_i ai,从顶点 v j v_j vj 到顶点 v ( j + a i ) m o d d v_{(j + a_i) \ mod \ d} v(j+ai) mod d 建权值为 ⌊ j + a i d ⌋ − ⌊ j d ⌋ \lfloor \frac{j + a_i}{d} \rfloor - \lfloor \frac{j}{d} \rfloor ⌊dj+ai⌋−⌊dj⌋ 的有向边,其中 0 ≤ j < d 0 ≤ j < d 0≤j<d。则原问题转化为该图上从顶点 v 0 v_0 v0 到其自身的非零最短环,第一步特殊处理后转化为最短路问题。
注意到边权值非负,故可用 Dijkstra 最短路算法即可解决该问题。代码如下:
#include<algorithm>
#include<stdio.h>
#define MAX_D (40005)
#define MAX_N (105)
#define HeapAsn(h,i,j,k) h[j]=k,i[h[j]]=j
#define HeapIns(h,i,k,cmp) \
{ \
int __tmp_i=i[k]; \
if(__tmp_i==-1) \
__tmp_i=++h[0]; \
while(__tmp_i>1) \
if(cmp(k,h[__tmp_i/2])) \
HeapAsn(h,i,__tmp_i,h[__tmp_i/2]), \
__tmp_i/=2; \
else \
break; \
HeapAsn(h,i,__tmp_i,k); \
}
#define HeapExt(h,i,cmp) \
({ \
int __tmp_i=1,__tmp_k=h[1]; \
i[h[1]]=-1; \
while(__tmp_i*2<h[0]) \
{ \
int __tmp_j=__tmp_i*2; \
if(__tmp_j+1<h[0]&&cmp(h[__tmp_j+1],h[__tmp_j])) \
__tmp_j++; \
if(cmp(h[__tmp_j],h[h[0]])) \
HeapAsn(h,i,__tmp_i,h[__tmp_j]), \
__tmp_i=__tmp_j; \
else \
break; \
} \
HeapAsn(h,i,__tmp_i,h[h[0]--]); \
__tmp_k; \
})
#define cmpDst(p,q) (dst[p]<dst[q])
int a[MAX_N],dst[MAX_D],hep[MAX_D],idx[MAX_D];
int main()
{
int d,n,r,s,t;
while(1)
{
scanf("%d %d",&n,&d);
if(n==0&&d==0)
break;
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
std::sort(a,a+n);
for(int i=0;i<d;i++)
dst[i]=idx[i]=-1;
hep[0]=0;
for(int i=0;i<n;i++)
if(dst[a[i]%d]==-1)
{
dst[a[i]%d]=a[i]/d;
HeapIns(hep,idx,a[i]%d,cmpDst);
}
while(hep[0]>0)
{
r=HeapExt(hep,idx,cmpDst);
for(int i=0;i<n;i++)
{
s=dst[r]+(r+a[i])/d,
t=(r+a[i])%d;
if(dst[t]==-1||dst[t]>s)
{
dst[t]=s;
HeapIns(hep,idx,t,cmpDst);
}
}
}
printf("%d\n",dst[0]);
}
return 0;
}
G
题目大意:二分图最大匹配。
匈牙利算法,最大流算法均可。本题数据可能存在较大问题(过弱)。代码如下:
#include<stdio.h>
#define MAX_K (1005)
#define MAX_N (1005)
struct E
{
int to;
E *nxt;
}edg[MAX_K+MAX_K],*fdg[MAX_N+MAX_N];
int m,mtc[MAX_N];
bool dfn[MAX_N];
bool dfs(int n)
{
dfn[n-m]=true;
for(E *i=fdg[n];i!=NULL;i=i->nxt)
if(!dfn[mtc[i->to]-m]&&(mtc[i->to]==-1||dfs(mtc[i->to])))
{
mtc[i->to]=n;
return true;
}
return false;
}
int main()
{
int k,n,t;
scanf("%d",&t);
while(t--)
{
scanf("%d %d %d",&n,&m,&k);
for(int i=1;i<=m+n;i++)
fdg[i]=NULL;
while(k--)
scanf("%d %d",&edg[k*2].to,&edg[k*2+1].to),edg[k*2].to+=m,
edg[k*2].nxt=fdg[edg[k*2+1].to],fdg[edg[k*2+1].to]=&edg[k*2],
edg[k*2+1].nxt=fdg[edg[k*2].to],fdg[edg[k*2].to]=&edg[k*2+1];
k=0;
for(int i=1;i<=m;i++)
mtc[i]=-1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
dfn[j]=false;
if(dfs(i+m))
k++;
}
printf("%d\n",k);
}
return 0;
}
H
题目大意:在 N * M 棋盘上,求马由 (1, 1) 到达 (a, b) 最小步数。数据范围 1 0 18 10^{18} 1018。
笔者能力所限,未能解决该问题。
笔者的想法是,min(a, b) ≤ 3 时找规律,小范围数据暴力解决,大数据找规律。翻转使得终点位于 3 到 4 点半方向,当位于 3 到 4 点钟方向时,通过最多三次 1 点钟或 5 点钟马步和若干可计算次 2 点钟和 4 点钟马步到达;当位于 4 到 4 点半方向时,通过最多二次 2 点钟或 7 点钟马步和若干可计算次 4 点钟和 5 点钟马步到达。不过,该想法仍存在问题。
代码如下,不正确,仅供参考:
#include<stdio.h>
#define Read(x) \
{ \
char __tmp_c=getchar(); \
while(__tmp_c<'0'||__tmp_c>'9') \
__tmp_c=getchar(); \
for(x=0;__tmp_c>='0'&&__tmp_c<='9';__tmp_c=getchar()) \
x=x*10+(__tmp_c-'0'); \
}
#define Write(x) \
{ \
long long __tmp_x,__tmp_y=0; \
for(__tmp_x=x;__tmp_x>0;__tmp_x/=10) \
__tmp_y=__tmp_y*10+__tmp_x%10; \
for(;__tmp_y>0;__tmp_y/=10) \
putchar(__tmp_y%10+'0'); \
if(x==0) \
putchar('0'); \
putchar('\n'); \
}
int main()
{
int t;
long long a,b,m,n,r;
scanf("%d",&t);
while(t--)
{
Read(n);Read(m);Read(a);Read(b); // 快速读写,原题用 cin/cout 超时
a--,b--; // 起点 (0, 0),方便运用公式
if(a>b) // 保证 a<=b,用对称性简化情形
r=n,n=m,m=r,
r=a,a=b,b=r;
if(n==1||m==1) // n*1 或 1*m 棋盘
if(a==0&&b==0)
r=0;
else
r=-1;
else if(n==2||m==2) // n*2 或 2*m 棋盘
if(a*2==b%4) // 形如 (0, 4k), (1, 4k+2) 的点可到达
r=b/2;
else
r=-1;
else if(n==3&&m==3&&a==1&&b==1) // 3*3 棋盘,(1, 1) 特判,其它与通常情形相同
r=-1;
else if(n>=3&&m==4&&a==0&&b==3) // n*4 棋盘,(0, 3) 特判,其它与通常情形相同
r=5;
else if(a==0&&b==1) // 平凡棋盘,(0, 1) 特判,非公式情形
r=3;
else if(b==1||a==2&&b==2) // 平凡棋盘,(1, 1), (2, 2) 特判,非公式情形
r=4;
else if(a*2<b) // 公式情形,3 点到 4 点钟方向,分 4 种子情况
{
r=(b-a*2)%4;
if(r>=2)
r--; // 此时 r 是 1 点和 5 点和 7 点和 11 点钟马步数
r+=b/2; // b/2 是 2 点和 4 点钟马步数
}
else // 公式情形,4 点到 4 点半方向,分 3 种子情况
r=(a+b)/3+(a+b)%3; // (a+b)/3 是 4 点和 5 点钟马步数,(a+b)%3 是 2 点和 7 点钟马步数
if(r!=-1)
Write(r)
else
printf("-1\n");
}
return 0;
}
I
题目大意:给 n 个非负整数 x 1.. n x_{1 .. n} x1..n,将其划为三部分,设三部分数个数分别为 n 1 n_1 n1、 n 2 n_2 n2、 n 3 n_3 n3,每部分数中最大值分别为 h 1 h_1 h1、 h 2 h_2 h2、 h 3 h_3 h3,求 ( n 1 + n 3 ) ∗ m a x ( h 1 , h 3 ) + n 2 ∗ h 2 (n_1 + n_3) * max(h_1, h_3) + n_2 * h_2 (n1+n3)∗max(h1,h3)+n2∗h2 的最小值。 n 1 n_1 n1、 n 2 n_2 n2、 n 3 n_3 n3 可以为 0。
将原数列首尾相接形成数环,则原问题等价于将数环分为两部分,数个数分别为 n 1 n_1 n1、 n 2 n_2 n2,最大值分别为 h 1 h_1 h1、 h 2 h_2 h2,求 n 1 ∗ h 1 + n 2 ∗ h 2 n_1 * h_1 + n_2 * h_2 n1∗h1+n2∗h2 的最小值。令 h 1 = m a x ( x 1.. n ) h_1 = max(x_{1 .. n}) h1=max(x1..n),即第一部分包含数列中最大值,可得 h 1 ≥ h 2 h_1 ≥ h_2 h1≥h2。考虑任一种方案,第二部分为 x L 2 x_{L2} xL2 到 x R 2 x_{R2} xR2(则在数环意义上,第一部分为 x R 2 + 1 x_{R2 + 1} xR2+1 到 x L 2 − 1 x_{L2 - 1} xL2−1),对于 x L 2 − 1 x_{L2 - 1} xL2−1 和 x L 2 x_{L2} xL2 之间的关系,若有 x L 2 − 1 ≤ x L 2 x_{L2 - 1} ≤ x_{L2} xL2−1≤xL2,则将 x L 2 − 1 x_{L2 - 1} xL2−1 划给第二部分的方案显然更优(因为如此一来, n 1 n_1 n1 减一, n 2 n_2 n2 加一, h 2 h_2 h2 不变且仍小于 h 1 h_1 h1)。于是,最优方案必有如下性质: x L 2 − 1 > x L 2 x_{L2 - 1} > x_{L2} xL2−1>xL2 且 x R 2 < x R 2 + 1 x_{R2} < x_{R2 + 1} xR2<xR2+1。
搜索,每次选取数环第二部分中最大数位置,将最大数连同其左边所有数或右边所有数全部划为第一部分,更新最优解,并递归搜索下去。最优解如上性质必定会被搜索到,即使存在相同数也不影响搜索顺序(若相同数比最优解 h 2 h_2 h2 大,则相同数全部划为第一部分后必存在一颗搜索子树为最优解;若相同数不比最优解 h 2 h_2 h2 大,则在搜索相同数之前就已经搜索到最优解)。处理时通过将数列最大值循环移动至数列首部使得第二部分连续,并需 ST 表实现 RMQ 功能。代码如下:
#include<stdio.h>
#define MAX_N (10000005)
#define MAX_2N (0x1FFFFFF)
#define INF (0x7FFFFFFFFFFFFFFFll)
#define MinLL(a,b) \
({ \
long long __tmp_a=(a),__tmp_b=(b); \
__tmp_a<__tmp_b?__tmp_a:__tmp_b; \
})
#define MaxInx(a,b) \
({ \
int __tmp_a=(a),__tmp_b=(b); \
x[__tmp_a]>x[__tmp_b]?__tmp_a:__tmp_b; \
})
int m,n,st[MAX_2N],x[MAX_N],xt[MAX_N];
long long ans;
int MaxST(int f,int l,int r,int p,int m)
{
if(l>r)
return 0;
int lt=p<<f,md=p<<f|1<<f-1,rt=p+1<<f;
if(l<=lt&&r>=rt-1)
return st[1<<m-f|p];
if(r<md)
return MaxST(f-1,l,r,p*2,m);
if(l>=md)
return MaxST(f-1,l,r,p*2+1,m);
return MaxInx(MaxST(f-1,l,r,p*2,m),MaxST(f-1,l,r,p*2+1,m));
}
void dfs(int lt,int rt)
{
if(lt==rt)
{
ans=MinLL(ans,(n-1)*(long long)x[0]+x[lt]);
return;
}
if((n-rt+lt-1)*(long long)x[0]>ans)
return;
int md=MaxST(m,lt,rt,0,m);
ans=MinLL(ans,(n-rt+lt-1)*(long long)x[0]+(rt-lt+1)*(long long)x[md]);
if(lt<md)
dfs(lt,md-1);
if(md<rt)
dfs(md+1,rt);
}
int main()
{
int a,b,mod,num,s,t;
scanf("%d",&t);
while(t--)
{
scanf("%d %d",&m,&n);
xt[0]=0;
while(m--)
{
scanf("%d %d %d %d %d",&num,&s,&a,&b,&mod);
while(num--)
xt[++xt[0]]=s,s=(s*(long long)a+b)%mod;
}
for(m=0,--xt[0];xt[0]>0;m++,xt[0]>>=1);
for(int i=1;i<=n;i++)
if(xt[xt[0]]<xt[i])
xt[0]=i;
for(int i=0;i<n;i++)
x[i]=xt[(xt[0]+i-1)%n+1];
for(int i=0;i<MAX_2N;i++)
st[i]=0;
for(int i=0;i<n;i++)
st[1<<m|i]=i;
for(int i=0;i<m;i++)
for(int j=0;j<<i+1<n;j++)
st[1<<m-i-1|j]=MaxInx(st[1<<m-i|j*2],st[1<<m-i|j*2+1]);
ans=INF;
dfs(1,n-1);
printf("%lld\n",ans);
}
return 0;
}
J
题目大意:求 N! 的因子中有多少数恰有 75 个因子。
一个数有奇数个因子当且仅当它为完全平方数。 75 = 3 ∗ 5 ∗ 5 = 5 ∗ 15 = 3 ∗ 25 = 75 75 = 3 * 5 * 5 = 5 * 15 = 3 * 25 = 75 75=3∗5∗5=5∗15=3∗25=75,即有 75 个因子的数 X = p 1 2 ∗ p 2 4 ∗ p 3 4 X = p_1^2 * p_2^4 * p_3^4 X=p12∗p24∗p34 或 X = p 1 4 ∗ p 2 14 X = p_1^4 * p_2^{14} X=p14∗p214 或 X = p 1 2 ∗ p 2 24 X = p_1^2 * p_2^{24} X=p12∗p224 或 X = p 1 74 X = p_1^{74} X=p174( p 1 , p 2 , p 3 p_1,p_2,p_3 p1,p2,p3 均为质数且互不相同)。
3 / 2 + 3 / 4 = 1 + 0 = 1
4 / 2 + 4 / 4 = 2 + 1 = 3
5 / 2 + 5 / 4 = 2 + 1 = 3
6 / 2 + 6 / 4 = 3 + 1 = 4
15 / 2 + 15 / 4 + 15 / 8 + 15 / 16 = 7 + 3 + 1 + 0 = 11
16 / 2 + 16 / 4 + 16 / 8 + 16 / 16 = 8 + 4 + 2 + 1 = 15
27 / 2 + 27 / 4 + 27 / 8 + 27 / 16 = 13 + 6 + 3 + 1 = 23
28 / 2 + 28 / 4 + 28 / 8 + 28 / 16 = 14 + 7 + 3 + 1 = 25
77 / 2 + 77 / 4 + 77 / 8 + 77 / 16 + 77 / 32 + 77 / 64 = 38 + 19 + 9 + 4 + 2 + 1 = 73
78 / 2 + 78 / 4 + 78 / 8 + 78 / 16 + 78 / 32 + 78 / 64 = 39 + 19 + 9 + 4 + 2 + 1 = 74
4! 可提供 2 个质因子 2,6! 可提供 4 个质因子 2,16! 可提供 14 个质因子 2,28! 可提供 24 个质因子 2,78! 可提供 74 个质因子 2。
5 / 3 = 1
6 / 3 = 2
8 / 3 + 8 / 9 = 2 + 0 = 2
9 / 3 + 9 / 9 = 3 + 1 = 4
29 / 3 + 29 / 9 + 29 / 27 = 9 + 3 + 1 = 13
30 / 3 + 30 / 9 + 30 / 27 = 10 + 3 + 1 = 14
53 / 3 + 53 / 9 + 53 / 27 = 17 + 5 + 1 = 23
54 / 3 + 54 / 9 + 54 / 27 = 18 + 6 + 2 = 26
6! 可提供 2 个质因子 3,9! 可提供 4 个质因子 3,30! 可提供 14 个质因子 3,54! 可提供 24 个质因子 3。
59 / 5 + 59 / 25 = 11 + 2 = 13
60 / 5 + 60 / 25 = 12 + 2 = 14
99 / 5 + 99 / 25 = 19 + 3 = 22
100 / 5 + 100 / 25 = 20 + 4 = 24
60! 可提供 14 个质因子 5,100! 可提供 24 个质因子 5。
90 / 7 + 90 / 49 = 12 + 1 = 13
91 / 7 + 91 / 49 = 13 + 1 = 14
91! 可提供 14 个质因子 7。
当 p > 4 时,(2p)! 可提供 2 个质因子 p,(4p)! 可提供 4 个质因子 p。
打表即可。代码如下:
#include<stdio.h>
#define MAX_N (105)
bool prime(int x)
{
if(x<2)
return false;
for(int i=2;i*i<=x;i++)
if(x%i==0)
return false;
return true;
}
int ans[MAX_N];
int main()
{
int n,s02,s04,s14,s24;
ans[0]=s02=s04=s14=s24=0;
for(int i=1;i<=100;i++)
{
ans[i]=ans[i-1];
if(i%2==0&&prime(i/2))
ans[i]+=s04*(s04-1)/2+s24,s02++;
if(i==6||i==9||i!=8&&i!=12&&i%4==0&&prime(i/4))
ans[i]+=(s02-2)*s04+s14,s04++;
if(i==16||i==30||i==60||i==91)
ans[i]+=s04-1,s14++;
if(i==28||i==54||i==100)
ans[i]+=s02-1,s24++;
if(i==78)
ans[i]++;
}
while(scanf("%d",&n)==1)
printf("%d\n",ans[n]);
return 0;
}
K
题目大意:给 N 个点无根树,边权为 1,求路径长为奇的路径数。
以点 1 为根建树,设 S i , 0 S_{i, 0} Si,0 为以 i 为根子树中距根长度为偶数的点数(包括根), S i , 1 S_{i, 1} Si,1 为以 i 为根子树中距根长度为奇数的点数。考虑点对 (s, t),设其最近公共祖先为 lca(s, t),则路径 s - lca(s, t) - t 和路径 s - lca(s, t) - 1 - lca(s, t) - t 长度奇偶性相同,树上任一路径均可看作经过根的长度奇偶性相同的等价路径,由奇数 = 偶数 + 奇数可知,则 S 1 , 0 ∗ S 1 , 1 S_{1, 0} * S_{1, 1} S1,0∗S1,1 即为所求。代码如下:
#include<stdio.h>
#define MAX_N (100005)
int s[MAX_N][2];
struct E
{
int to;
E *nxt;
}edg[MAX_N+MAX_N],*vtx[MAX_N];
void dfs(int x)
{
s[x][0]=1,s[x][1]=0;
for(E *i=vtx[x];i!=NULL;i=i->nxt)
if(s[i->to][0]==0)
dfs(i->to),
s[x][0]+=s[i->to][1],
s[x][1]+=s[i->to][0];
}
int main()
{
int t,n;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
vtx[i]=NULL,s[i][0]=0;
for(int i=1;i<n;i++)
scanf("%d %d",&edg[i*2].to,&edg[i*2-1].to),
edg[i*2].nxt=vtx[edg[i*2-1].to],vtx[edg[i*2-1].to]=&edg[i*2],
edg[i*2-1].nxt=vtx[edg[i*2].to],vtx[edg[i*2].to]=&edg[i*2-1];
dfs(1);
printf("%lld\n",s[1][0]*(long long)s[1][1]);
}
return 0;
}
L
题目大意:给 n ( ≤ 1 0 9 ) , k ( ≤ 1000 ) , a 1.. k , f 0.. k − 1 n(≤ 10^9), k(≤ 1000), a_{1 .. k}, f_{0 .. k - 1} n(≤109),k(≤1000),a1..k,f0..k−1,已知 f n = ∑ j = 1 k a j ∗ f n − j f_n = \sum_{j=1}^k a_j*f_{n-j} fn=∑j=1kaj∗fn−j,求 f n f_n fn mod 1e9+7。
矩阵快速幂+多项式优化。令
χ
m
=
[
f
m
f
m
−
1
f
m
−
2
.
.
.
f
m
−
k
+
1
]
,
A
=
[
a
1
a
2
.
.
.
a
k
−
1
a
k
1
0
.
.
.
0
0
0
1
.
.
.
0
0
.
.
.
0
0
.
.
.
1
0
]
\chi_m=\begin{bmatrix} f_m \\ f_{m-1} \\ f_{m-2} \\ ... \\ f_{m-k+1} \end{bmatrix}, A=\begin{bmatrix} a_1 & a_2 & ... & a_{k-1} & a_k \\ 1 & 0 & ... & 0 & 0 \\ 0 & 1 & ... & 0 & 0 \\ ... \\ 0 & 0 & ... & 1 & 0 \end{bmatrix}
χm=
fmfm−1fm−2...fm−k+1
,A=
a110...0a2010............ak−1001ak000
则有
[
f
m
f
m
−
1
.
.
.
f
m
−
k
+
1
]
=
χ
n
=
A
χ
n
−
1
=
.
.
.
=
A
n
χ
0
=
A
n
[
f
0
f
−
1
.
.
.
f
1
−
k
]
\begin{bmatrix} f_m \\ f_{m-1} \\ ... \\ f_{m-k+1} \end{bmatrix}=\chi_n=A\chi_{n-1}=...=A^n\chi_0 = A^n\begin{bmatrix} f_0 \\ f_{-1} \\ ... \\ f_{1-k} \end{bmatrix}
fmfm−1...fm−k+1
=χn=Aχn−1=...=Anχ0=An
f0f−1...f1−k
用矩阵快速幂求得
A
n
A^n
An,即是朴素的矩阵快速幂算法。下面是多项式优化的内容。求矩阵
A
A
A 的特征多项式
∣
x
I
−
A
∣
=
∣
x
−
a
1
−
a
2
.
.
.
−
a
k
−
1
−
a
k
−
1
x
.
.
.
0
0
0
−
1
.
.
.
0
0
.
.
.
0
0
.
.
.
−
1
x
∣
=
x
k
−
a
1
x
k
−
1
−
a
2
x
k
−
2
−
.
.
.
−
a
k
x
0
=
p
(
x
)
\left| xI-A \right| = \begin{vmatrix} x-a_1 & -a_2 & ... & -a_{k-1} & -a_k \\ -1 & x & ... & 0 & 0 \\ 0 & -1 & ... & 0 & 0 \\ ... \\ 0 & 0 & ... & -1 & x \end{vmatrix} = x^k - a_1x^{k-1} - a_2x^{k-2} - ... - a_kx^0 = p(x)
∣xI−A∣=
x−a1−10...0−a2x−10............−ak−100−1−ak00x
=xk−a1xk−1−a2xk−2−...−akx0=p(x)
由特征多项式性质
p
(
A
)
=
p
(
x
)
∣
x
=
A
=
O
p(A)=p(x)|_{x=A}=O
p(A)=p(x)∣x=A=O,得(等式右边的
O
O
O 为全零方阵)
A
k
−
a
1
A
k
−
1
−
a
2
A
k
−
2
−
.
.
.
−
a
k
A
0
=
O
A^k - a_1A^{k-1} - a_2A^{k-2} - ... - a_kA^0 = O
Ak−a1Ak−1−a2Ak−2−...−akA0=O
这说明
A
m
(
∀
m
∈
N
)
A^m(\forall m\in\mathbb{N})
Am(∀m∈N) 可由
A
k
−
1
,
A
k
−
2
,
.
.
.
,
A
0
A^{k-1}, A^{k-2}, ..., A^0
Ak−1,Ak−2,...,A0 这
k
−
1
k-1
k−1 个方阵线性表示。用多项式取模运算,求得
x
n
=
p
(
x
)
q
(
x
)
+
r
(
x
)
r
(
x
)
=
r
k
−
1
x
k
−
1
+
r
k
−
2
x
k
−
2
+
.
.
.
+
r
0
x
0
x^n=p(x)q(x)+r(x) \\ r(x)=r_{k-1}x^{k-1}+r_{k-2}x^{k-2}+...+r_0x^0
xn=p(x)q(x)+r(x)r(x)=rk−1xk−1+rk−2xk−2+...+r0x0
其中
p
(
x
)
p(x)
p(x) 为上文的特征多项式(最高
k
k
k 次),
r
(
x
)
r(x)
r(x) 为不超过
k
−
1
k-1
k−1 次的多项式,
x
n
x^n
xn 取模后各项系数可以用快速幂求。带入
A
A
A,即有
A
n
=
r
(
A
)
=
r
k
−
1
A
k
−
1
+
r
k
−
2
A
k
−
2
+
.
.
.
+
r
0
A
0
A^n=r(A)=r_{k-1}A^{k-1}+r_{k-2}A^{k-2}+...+r_0A^0
An=r(A)=rk−1Ak−1+rk−2Ak−2+...+r0A0
两边同时右乘
χ
0
\chi_0
χ0,得
[
f
n
f
n
−
1
.
.
.
f
n
−
k
+
1
]
=
A
n
χ
0
=
r
k
−
1
A
k
−
1
χ
0
+
r
k
−
2
A
k
−
2
χ
0
+
.
.
.
+
r
0
A
0
χ
0
=
r
k
−
1
[
f
k
−
1
f
k
−
2
.
.
.
f
0
]
+
r
k
−
2
[
f
k
−
2
f
k
−
3
.
.
.
f
−
1
]
+
.
.
.
+
r
0
[
f
0
f
−
1
.
.
.
f
1
−
k
]
\begin{bmatrix} f_n \\ f_{n-1} \\ ... \\ f_{n-k+1} \end{bmatrix}=A^n\chi_0=r_{k-1}A^{k-1}\chi_0+r_{k-2}A^{k-2}\chi_0+...+r_0A^0\chi_0=r_{k-1}\begin{bmatrix} f_{k-1} \\ f_{k-2} \\ ... \\ f_0 \end{bmatrix}+r_{k-2}\begin{bmatrix} f_{k-2} \\ f_{k-3} \\ ... \\ f_{-1} \end{bmatrix}+...+r_0\begin{bmatrix} f_0 \\ f_{-1} \\ ... \\ f_{1-k} \end{bmatrix}
fnfn−1...fn−k+1
=Anχ0=rk−1Ak−1χ0+rk−2Ak−2χ0+...+r0A0χ0=rk−1
fk−1fk−2...f0
+rk−2
fk−2fk−3...f−1
+...+r0
f0f−1...f1−k
取第一行,得
f
n
=
r
k
−
1
f
k
−
1
+
r
k
−
2
f
k
−
2
+
.
.
.
+
r
0
f
0
f_n=r_{k-1}f_{k-1}+r_{k-2}f_{k-2}+...+r_0f_0
fn=rk−1fk−1+rk−2fk−2+...+r0f0
算法时间复杂度
O
(
k
2
log
n
)
O(k^2\log n)
O(k2logn),如果模数符合 NTT 特征,时间复杂度能降到
O
(
k
log
k
log
n
)
O(k\log k\log n)
O(klogklogn)。
多项式取模(系数反转),多项式求逆(倍增)的算法不再赘述。代码如下,只测过样例,仅供参考:
#include<stdio.h>
#define MAX_K (2050)
#define NUM_MOD (1000000007)
#define UINT_I(x) int x=1
#define UINT_MULMOD(a,b) (a*(long long)b%NUM_MOD)
#define IADD(x,y) x=(x+y)%NUM_MOD
#define POLY_I(x) POLY x={.siz=1,.coe={1}}
#define POLY_MULMOD(a,b) PolyMod(PolyMul(a,b))
#define POWER(a,n,T) \
({ \
int __tmp_n=n; \
T##_I(r); \
while(__tmp_n>0) \
{ \
if(__tmp_n&1==1) \
r=T##_MULMOD(a,r); \
a=T##_MULMOD(a,a); \
__tmp_n>>=1; \
} \
r; \
})
struct POLY
{
int siz,coe[MAX_K];
}a,av;
POLY PolyMul(POLY a,POLY b)
{
POLY r={.siz=a.siz+b.siz-1,.coe={0}};
for(int i=0;i<a.siz;i++)
if(a.coe[i]!=0)
for(int j=0;j<b.siz;j++)
IADD(r.coe[i+j],UINT_MULMOD(a.coe[i],b.coe[j]));
return r;
}
POLY PolyRev(POLY x)
{
POLY r={.siz=x.siz,.coe={0}};
for(int i=0;i<x.siz;i++)
r.coe[i]=x.coe[x.siz-i-1];
return r;
}
POLY PolySub(POLY a, POLY b)
{
POLY r={.siz=b.siz,.coe={0}};
if(a.siz>b.siz)
r.siz=a.siz;
for(int i=0;i<a.siz;i++)
IADD(r.coe[i],a.coe[i]);
for(int i=0;i<b.siz;i++)
IADD(r.coe[i],NUM_MOD-b.coe[i]);
return r;
}
POLY PolyDiv(POLY x)
{
if(x.siz<a.siz)
return (POLY){.siz=0,.coe={0}};
POLY xr=PolyRev(x);
av.siz=x.siz-a.siz+1;
POLY r=PolyMul(xr,av);
r.siz=av.siz,av.siz=a.siz-1;
return PolyRev(r);
}
POLY PolyMod(POLY x)
{
POLY q=PolyDiv(x);
POLY r=PolySub(x,PolyMul(q,a));
r.siz=a.siz-1;
return r;
}
POLY PolyInv(POLY x,int p)
{
POLY r={.siz=1,.coe={POWER(x.coe[0],NUM_MOD-2,UINT)}},t;
for(int i=2;i<=p;i<<=1)
{
x.siz=i;
t=PolyMul(x,r);
for(int i=0;i<t.siz;i++)
t.coe[i]=(NUM_MOD-t.coe[i])%NUM_MOD;
IADD(t.coe[0],2);
r=PolyMul(t,r);
r.siz=i;
}
r.siz=p;
return r;
}
int f[MAX_K];
int main()
{
int k,n,s;
scanf("%d%d",&n,&k);
for(int i=k-1;i>=0;i--)
scanf("%d",&s),
a.coe[i]=(NUM_MOD-s)%NUM_MOD;
for(int i=0;i<k;i++)
scanf("%d",&s),
f[i]=(NUM_MOD+s)%NUM_MOD;
a.siz=k+1,a.coe[k]=1;
POLY res={.siz=2,.coe={0,1}};
av=PolyInv(PolyRev(a),k);
res=POWER(res,n,POLY);
s=0;
for(int i=0;i<k;i++)
IADD(s,UINT_MULMOD(res.coe[i],f[i]));
printf("%d",s);
return 0;
}
Epilogue
笔者有幸参与此次比赛,无奈才疏学浅,仅 AC 二题(B 题和 K 题)。L 题开始想借矩阵快速幂之力,但矩阵乘法时间复杂度 k 3 k^3 k3 直接 TLE。J 题考场中程序已然写成,不幸代码中一个常数算错而 WA。E 题代码 WA 而检查无果,考后发现一条调试输出语句忘删。HF 题均非力所能及,直接放弃。ACG 题未加细看便直接放弃,但事实上均可做。D 题现场便想出数剖套线段树的做法,但数剖代码过长线段树长时未写而放弃。最后编写 I 题,直到考试结束都笔者都未能将 I 题编完,不过现在看来当时的贪心思路偏差较大,就算编完也只能以 WA 收尾。从此次比赛看来,HIT 藏龙卧虎,算法竞赛之路任重而道远。