0%

BUAA Campus Programming Contest 2018 预赛

从十二月份咕到三月份,然后再从第二周咕到第四周的校赛终于开始啦。

虽然是网络预赛,不过题目还是相当有意思的。

C 小张的魔法跳越

n(105) 个节点的树中,每条边有 unsigned int 的权值 w,令点对 (i,j) 的最短路径权和为 S(i,j),最短路径上所有边权异或和为 X(i,j),求 i<jS(i,j)×K(i,j)(mod109+7)

题解

对边权的每一位单独考虑,则就变成求每一位上异或和为 1 的点对之间的边权和。

类似树上背包的思想,令 f(u,0) f(u,1) 表示对节点 u 当前已经合并的子树中,路径异或和为 0 和 1 的路径的边权之和。更新时,只要用已经处理过的子树,和准备要加入的子树 v f(v,0) f(v,1) 更新答案,再将 f(v,0) f(v,1) 合并到 f(u,0) f(u,1) 中即可。

总时间复杂度 O(n)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;

const int N=100000+5;
const int MOD=1000000007;

typedef unsigned int ui;
typedef long long ll;

struct Adj{int v; ui w;};
vector<Adj> a[N];

int ans=0;
int f[N][2],cnt[N][2]; //节点i异或和为x时的边权和与数量

inline bool Judge(ui w,ui p){
return (w&p)>0;
}

void DFS(int u,int pa,ui p){
cnt[u][0]=1;
cnt[u][1]=f[u][0]=f[u][1]=0;

for(int i=0;i<a[u].size();i++){
int v=a[u][i].v;
ui w=a[u][i].w;

if(v==pa)continue;
DFS(v,u,p);
int b=Judge(w,p);

int f1 = (f[v][1^b] + (ll)w*cnt[v][1^b]) %MOD;
int f2 = (f[v][0^b] + (ll)w*cnt[v][0^b]) %MOD;

//0 1
ans=( ans + (ll)f[u][0] * cnt[v][1^b] %MOD * p )%MOD;
ans=( ans + (ll)f1 * cnt[u][0] %MOD * p )%MOD;

//1 0
ans=( ans + (ll)f[u][1] * cnt[v][0^b] %MOD * p )%MOD;
ans=( ans + (ll)f2 * cnt[u][1] %MOD * p )%MOD;

cnt[u][0]=(cnt[u][0]+cnt[v][0^b])%MOD;
cnt[u][1]=(cnt[u][1]+cnt[v][1^b])%MOD;
f[u][0]=(f[u][0]+f2)%MOD;
f[u][1]=(f[u][1]+f1)%MOD;
}
}

int main(){
int nCase; scanf("%d",&nCase);

while(nCase--){
int n; scanf("%d",&n);
for(int i=1;i<=n;i++)
a[i].clear();

for(int i=1;i<n;i++){
int u,v; ui w;
scanf("%d%d%ud",&u,&v,&w);
a[u].push_back((Adj){v,w});
a[v].push_back((Adj){u,w});
}

ans=0;
for(int i=0;i<32;i++)
DFS(1,0,((ui)1)<<i);

printf("%d\n",ans);
}

return 0;
}

E 禁忌的共鸣

n(105) 个数 ai(105),任意两个数 (i,j) 之间有一条 (ai,aj) 的边,求这个图的最大生成树。

题解

显然不能直接搞图出来然后跑 Kruskal。考虑 Kruskal 的贪心原则,从大到小枚举约数,然后将含有这个约数的点全部加入并查集即可。

很诡异的时间复杂度。据说约数个数是 n/lnn 级别的,所以我提前处理好每个约数对应的 ai 的时间复杂度理论上是 O(n3/2/lnn) 级别的。

Potassium点我跳转)讨论了一下,他是先枚举了约数 p,然后判断是否存在 i×p=ai。这样的话复杂度是有保证的 i=1nn/i=O(nlogn) 级别。

果然还是不能什么都用 vector

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<iostream>
#include<ctime>
#include<vector>
using namespace std;

typedef long long ll;

const int N=100000+5;
vector<int> fac[N]; //maxsize: 128
vector<int> edg[N]; //含有同一因子的边
int n;

void FindFac(){
for(int i=1;i<N;i++)
for(int j=i;j<N;j+=i)
fac[j].push_back(i);
}

int pa[N];

int FindPa(int x){
return pa[x]==x?x:pa[x]=FindPa(pa[x]);
}

ll Kruskal(){
ll ans=0; int cnt=0;
for(int i=N-1;i>0;i--){
if(edg[i].size()>1){
for(int j=1;j<edg[i].size();j++){
int u=edg[i][0],v=edg[i][j];
int paU=FindPa(u),paV=FindPa(v);
if(paU!=paV){
ans+=i; cnt++;
pa[paU]=paV;
if(cnt==n-1)return ans;
}
}
}
}
}

void Init(){
for(int i=1;i<=n;i++)
pa[i]=i;
for(int i=1;i<N;i++)
edg[i].clear();
}

int main(){
FindFac();

int nCase; scanf("%d",&nCase);
while(nCase--){
scanf("%d",&n);
Init();

for(int i=1,a;i<=n;i++){
scanf("%d",&a);
for(int j=0;j<fac[a].size();j++)
edg[fac[a][j]].push_back(i);
}

printf("%lld\n",Kruskal());
}

return 0;
}

F zzh 与同余方程

mS(i) i 相关的同余方程的解:

(i+1)i+1(i+1)i(modm)

多次询问,求 i=1n|S(i)|(mod998244353),其中 n107

题解

题目本质是要求 mi(i+1)i 的所有 m,也就是 i(i+1)i 的因数个数。注意到当 (i,i+1)=1,因此只要分别处理出每个数 i 有多少个不同的质因数和每个质因数的数目 ki 即可。

由约数个数公式可知,对 p=piki,有约数个数 D(p)=(ki+1)D(pq)=(kiq+1)。因此只要筛出所有素数,然后用素数筛掉它的倍数计算每个 p ki 即可。

时间复杂度:一个数的质因数分解中最多有 log2n 个数,每个数 p 都会被每个质因数一直筛到 1,因此时间复杂度是 O(nlogn) 的。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include<cstdio>
#include<iostream>
#include<vector>
#include<ctime>
using namespace std;

typedef long long ll;

const int N=10000000+5,MOD=998244353;
int ans[N];
bool notPri[N];
int pri[N];

void Euler(){
for(int i=2;i<N;i++){
if(!notPri[i])
pri[++pri[0]]=i;
for(int j=1;j<=pri[0]&&i*pri[j]<N;j++){
notPri[i*pri[j]]=1;
if(i%pri[j]==0)break;
}
}
}

void FindFac(){
for(int i=1;i<N;i++)ans[i]=1;

for(int i=1;i<=pri[0];i++){
for(int j=pri[i];j<N;j+=pri[i]){
int cnt=0,tmp=j;
while(tmp%pri[i]==0){
tmp/=pri[i];
cnt++;
}
ans[j-1]=1LL*ans[j-1]*(1LL*cnt*(j-1)%MOD+1)%MOD;
ans[j]=1LL*ans[j]*(cnt+1)%MOD;
}
}
}

void Process(){
for(int i=1;i<N;i++){
ans[i]=(ans[i]+ans[i-1])%MOD;
}
}

int main(){
Euler();
FindFac();
Process();

int nCase; scanf("%d",&nCase);
while(nCase--){
int x; scanf("%d",&x);
printf("%d\n",ans[x]);
}

return 0;
}

G 小 z 刷题

给定 n(105) 个数对 (ai,pi) 表示一个连续的序列,一个数对表示数列的一部分,该部分由 pi ai 构成。令 S(x) 表示该序列中长度为 x 的上升子序列个数,求 xk×S(x)(mod109+7),其中 k20

题解

很优美的一道题目。

假设已经知道前 n 个数中满足 ai<an+1 xk×S(x),考虑加入下一个数 an+1。显然在原有答案末尾加入 pn+1 an+1 后可以对答案做出贡献。下式进行二项式展开:

xpn+1×(x+1)k×S(x)=xpn+1×i=1k(ki)xi×S(x)=i=1k(ki)pn+1xxi×S(x)

即对新加入的结果,可以由满足 ai<an+1 部分的 xt×S(x),t=0,1,2,,k 结果计算出来。因此离散化之后可以用树状数组维护 t 次方下的答案并更新。

总时间复杂度 O(nlogn+nk2)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long ll;

const int N=100000+5,MOD=1000000007,K=20+2;

struct BITree{
int t[N];
BITree(){
Clear();
}
void Clear(){
memset(t,0,sizeof(t));
}
int Lowbit(int x){
return x&(-x);
}
void Add(int x,int v){
for(;x<N;x+=Lowbit(x))
t[x]=(t[x]+v)%MOD;
}
int Query(int x){
int ret=0;
for(;x;x-=Lowbit(x))
ret=(ret+t[x])%MOD;
return ret;
}
};

BITree b[K];
int v[N],p[N],pos[N]; //val*p个
int c[K][K];
int n,k;

void InitComb(){
for(int i=0;i<K;i++){
c[0][i]=1;
for(int j=1;j<=i;j++)
c[j][i]=(c[j][i-1]+c[j-1][i-1])%MOD;
}
}

int main(){
InitComb();

int nCase; scanf("%d",&nCase);
while(nCase--){
int n,k; scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d%d",&v[i],&p[i]);
pos[i]=v[i];
}
sort(pos+1,pos+n+1);

for(int i=0;i<=k;i++)
b[i].Clear();

int ans=0;
for(int i=1;i<=n;i++){
int idx=lower_bound(pos+1,pos+n+1,v[i])-pos;
int tmpSum[K];
for(int j=0;j<=k;j++)
tmpSum[j]=b[j].Query(idx-1);

for(int j=0;j<=k;j++){
int tmpAns=1;
for(int l=0;l<=j;l++)
tmpAns=(tmpAns+1LL*tmpSum[l]*c[l][j])%MOD;

tmpAns=1LL*tmpAns*p[i]%MOD;
b[j].Add(idx,tmpAns);

if(j==k)ans=(ans+tmpAns)%MOD;
}
}

printf("%d\n",ans);
}

return 0;
}

I Ange 的 DNA 自动机

定义矩阵:

A=[1001]T=[1001]C=[1001]G=[1001]

给定长度为 n(106) 的字符串 S={s1,s2,,sn}。定义一次操作 ST

ti=si×s(imodn)+1

求经过 k(109) 次这样操作后的字符串 T

题解

经过很显然的手推可以发现,令 A,T,G,C 分别代表 0,1,2,3,并定义乘法为异或操作的情况下,计算方法和原式等价。

假设 f(k,i) 表示操作 k 次后的串的第 i 个数,则再一次经过很显然的手推可以发现有如下关系:

f(k,i)=f(k2t,i)f(k2t,i+2t)

其中 表示异或操作,t 是满足 k2t>0 的最大 t(也就是 k 在二进制下的最高位)。这个递归是 O(logk) 级别的,因此总时间复杂度 O(nlogk)

中间传 vector 太多导致第一次 T 掉了。还是要看场合使用啊。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<iostream>
#include<cstdio>
#include<vector>
#include<ctime>
using namespace std;

const int N=1000000+5,K=32;
char ch[K][N];
int n;

int Highbit(int x){
for(int i=30;i>=0;i--)
if(x&(1<<i))return 1<<i;
return 0;
}

char* NextXOR(int t,int dep){ //a是原数组
if(t==0)return ch[0];
int valHB=Highbit(t);

char* pre=NextXOR(t-valHB,dep+1);
char* ret=ch[dep];

for(int i=0;i<n;i++)
ret[i]=(pre[i]^pre[(i+valHB)%n]);

return ret;
}

int main(){
int nCase; scanf("%d",&nCase);
while(nCase--){
int k; scanf("%d%d\n",&n,&k);

char* arr=ch[0];
for(int i=0;i<n;i++){
char c; scanf("%c",&c);
if(c=='A'){
arr[i]=0;
}else if(c=='T'){
arr[i]=1;
}else if(c=='G'){
arr[i]=2;
}else if(c=='C'){
arr[i]=3;
}
}

char* ret=NextXOR(k,1);

for(int i=0;i<n;i++){
if(ret[i]==0)
printf("A");
else if(ret[i]==1)
printf("T");
else if(ret[i]==2)
printf("G");
else if(ret[i]==3)
printf("C");
}

printf("\n");
}

return 0;
}

总结

是一套很有意思的题目和一位很菜的参赛者

有两处错误都是因为中途爆了 long longWA 掉了,并且一开始都没有找出原因来。深刻反省深刻反省。只要有 long long 和相乘的地方,都需要思考到底会不会爆 long long

有一个比较隐式的爆法是,x 每次都有可能变为 xp,其中 1p20p=1 的情况下相当于 x 每次都增加一倍,这个时候就会爆掉了。

明天的线下赛也要加油啊。