【解题报告】CSP-S2019

Day1

T1 格雷码

思路

一个模拟出来的搜索,当年我考场上得了个不知道多少分

当年找了个一个小时,结果是错的,也只有60pts

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
using namespace std;
unsigned long long n,k;
void dfs(unsigned long long n,unsigned long long k)
{
if(n==0)
return ;
long long x=(1<<(n-1));//表示有多少个
if(k==0)//如果k为0的话,没有好吧
{
for(long long i=1;i<=n;i++)
cout<<"0";
return ;
}
if(k+1>x)//如果要的数字在中间的右边
{
long long next=2*x-k-1;//计算对应右边的n-1位格雷码
cout<<"1";
dfs(n-1,next);
}
else//在左边就是正常的格雷码
{
long long next=k;
cout<<"0";
dfs(n-1,next);
}
}
int main()
{
//freopen("code.in","r",stdin);
//freopen("code.out","w",stdout);
cin>>n>>k;
dfs(n,k);
cout<<endl;
// fclose(stdin);
// fclose(stdout);
return 0;
}

然后我们考虑对的思路

我们手算出前几个格雷码,然后可以找到规律

我们就能进行一些操作了

毕竟,像这种输入一个或者两个数字然后输出一个数字的

要么就是有一个简单的递推式

要么就能打表找规律

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
int n;
unsigned long long k;
int main()
{
cin>>n>>k;
k^=k>>1;
while(~--n)
cout<<(k>>n&1);
return 0;
}

T2 括号树

思路

直接一个不用思考的暴力,可以有20pts

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 <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=500005;
int n,fa[maxn],ans;
bool flag=true;
char s[maxn];
struct edge{
int e,next;
}ed[maxn<<1];
int en,first[maxn];
void add_edge(int s,int e)
{
en++;
ed[en].next=first[s];
first[s]=en;
ed[en].e=e;
}
bool check(int l,int r)
{
int cnt=0;
for(int i=l;i<=r;i++)
{
if(s[i]=='(') cnt++;
else if(s[i]==')') cnt--;
if(cnt<0) return false;
}
if(cnt==0) return true;
return false;
}
int main()
{
cin>>n;
scanf("%s",s+1);
for(int i=1;i<=n-1;i++)
{
int x;
cin>>x;
fa[i+1]=x;
if(fa[i+1]!=i) flag=false;
add_edge(i+1,x);
add_edge(x,i+1);
}
if(flag)
{
for(int i=1;i<=n;i++)
{
int cnt=0;
for(int l=1;l<=i;l++)
{
for(int r=l+1;r<=i;r++)
{
if(check(l,r))
cnt++;
}
}
ans=(ans^(cnt*i));
}
cout<<ans<<'\n';
return 0;
}
return 0;
}

然后我们手推一下,到每个位置,一共有多少对括号

一定要手推!!! 不然是找不到规律的

然后我们发现

一个后括号如果能匹配一个前括号,假设这个前括号的前1位同样有一个已经匹配了的后括号,那么我们势必可以把当前的匹配和之前的匹配序列合并,当前的这个后括号的贡献值,其实就等于前面那个后括号的贡献值+1!

也就是这么一个性质,可以拿到55pts的高分

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int maxn=500005;
int n,fa[maxn],ans;
int st[maxn],don[maxn],sum[maxn],size;
bool flag=true;
char s[maxn];
struct edge{
int e,next;
}ed[maxn<<1];
int en,first[maxn];
void add_edge(int s,int e)
{
en++;
ed[en].next=first[s];
first[s]=en;
ed[en].e=e;
}
bool check(int l,int r)
{
int cnt=0;
for(int i=l;i<=r;i++)
{
if(s[i]=='(') cnt++;
else if(s[i]==')') cnt--;
if(cnt<0) return false;
}
if(cnt==0) return true;
return false;
}
signed main()
{
cin>>n;
scanf("%s",s+1);
for(int i=1;i<=n-1;i++)
{
int x;
cin>>x;
fa[i+1]=x;
if(fa[i+1]!=i) flag=false;
add_edge(i+1,x);
add_edge(x,i+1);
}
if(flag)
{
for(int i=1;i<=n;i++)
{
if(s[i]==')')
{
if(size==0) continue;
int t=st[size];
don[i]=don[t-1]+1;
size--;
}
else if(s[i]=='(')
st[++size]=i;
}
for(int i=1;i<=n;i++)
sum[i]=sum[i-1]+don[i];
for(int i=1;i<=n;i++)
ans=(ans^(i*sum[i]));
cout<<ans<<'\n';
return 0;
}
return 0;
}

然后我们发现

树上一个点到根的距离实际上也对应一个序列

我们可以对应每个点来计算,看他之前的括号排序是什么样子的

因为我们递归的时候有回溯

所以所有的答案不影响

所以实际上就是一个序列问题转化为树上问题

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int maxn=500005;
int n,fa[maxn],ans;
int st[maxn],don[maxn],sum[maxn],size;
bool flag=true;
char s[maxn];
struct edge{
int e,next;
}ed[maxn<<1];
int en,first[maxn];
void add_edge(int s,int e)
{
en++;
ed[en].next=first[s];
first[s]=en;
ed[en].e=e;
}

void dfs(int x)
{
int tmp=0;
if(s[x]==')')
{
if(size!=0)
{
tmp=st[size];
don[x]=don[fa[tmp]]+1;
size--;
}
}
else if(s[x]=='(')
st[++size]=x;
sum[x]=sum[fa[x]]+don[x];
for(int i=first[x];i;i=ed[i].next)
{
int e=ed[i].e;
dfs(e);
}
if(tmp!=0)
st[++size]=tmp;
else if(size)
size--;
}

bool check(int l,int r)
{
int cnt=0;
for(int i=l;i<=r;i++)
{
if(s[i]=='(') cnt++;
else if(s[i]==')') cnt--;
if(cnt<0) return false;
}
if(cnt==0) return true;
return false;
}
signed main()
{
cin>>n;
scanf("%s",s+1);
for(int i=2;i<=n;i++)
{
int x;
cin>>x;
add_edge(x,i);
fa[i]=x;
}
dfs(1);
for(int i=1;i<=n;i++)
ans=(ans^(i*sum[i]));
cout<<ans<<'\n';
return 0;
}

T3 树上的数

思路

这道题目具有里程碑意义

自从这道黑题之后,提高级的比赛在出黑题的时代大潮中一去不复返

直到现在还有深远的影响力,对一代又一代OIer留下了深刻印象

然而我还是不会

所以我们看一下

我们这道题目分部分分来进行研究

感谢@lyyi2003

  • 菊花图

菊花图的话有一个花心和一堆花瓣

然后我们根据手模可以发现

可以按照结点的编号和数字进行贪心

当我们处理一个点的时候,对于一个数字,枚举跟它相连的点

如果这个相邻的点没有被访问过,并且这个点是最后一个数字或者和之前这个数字并没有产生过直接联系

我们就以这个数字为排列的下一个数字

可以得到25pts

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
using namespace std;
const int maxn=4005;
int p[maxn],ans[maxn];
bool vis[maxn];
int fa[maxn];
int get(int x)
{
return fa[x]=(x==fa[x]? x:get(fa[x]));
}
int main()
{
int n,T;
cin>>T;
while(T--)
{
memset(vis,false,sizeof(vis));
cin>>n;
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=n;i++)
cin>>p[i];
int x,y;
for(int i=1;i<n;i++)
cin>>x>>y;
for(int i=1;i<=n;i++)
{
int x=p[i];
for(int j=1;j<=n;j++)
{
if(!vis[j]&&(i==n||get(x)!=get(j)))
{
ans[i]=j;
fa[get(x)]=get(j);
vis[j]=true;
break;
}
}
}
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
cout<<'\n';
}
return 0;
}

我们发现可以用拓扑序的模型来贪心

怎么贪呢

我们为每条边规定一个优先级,如果一条边的优先级大于另一条边的话,那么这条边必须造另一条边的后面删除

所以我们按照这个思路贪心,每次选一个当前数字能到达的,不和之前的条件冲突的,标号最小的结点作为这个数字最终所在的结点,然后将这个条件加入

这个怎么做呢,我们可以用一堆方法标记一下一条边和左右两条边的优先级,然后每次从当前数字开始找左右两边符合条件的点,然后把新条件对应的标记值更新就好了

然后我们继续考虑正解

我们按照上面的思路继续思考

我们发现,只有与同一个节点相邻的边之间才会有优先级的问题存在

我们对每一个点,开一个并查集,然后对每一个并查集建一个虚点

如果一条边的优先级最大,那么直接用虚点向他连一条边

如果一条边优先级最小,那么它向虚点连一条边然后套第一个25pts的代码,然后再结合上面的贪心思路,每次从一个点出发遍历整棵树,每走一条边就判断一次,然后对一条路径进行修改就好了

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=20005;
struct edge{
int e,next;
}ed[maxn];
int en,first[maxn];
void add_edge(int s,int e)
{
en++;
ed[en].next=first[s];
first[s]=en;
ed[en].e=e;
}

int fa[maxn],size[maxn],d[maxn],p[maxn],ans;

bool in[maxn],out[maxn];

int get(int x)
{
return fa[x]=(x==fa[x]? x:get(fa[x]));
}
void merge(int x,int y)
{
int u=get(x),v=get(y);
fa[u]=v,size[v]+=size[u];
out[x]=in[y]=true;
}
bool check(int x,int y,int l)
{
if(in[y]||out[x])
return false;
int u=get(x),v=get(y);
if(u==v&&size[u]!=l)
return false;
return true;
}
void dfs1(int v,int fath)
{
if(fath!=v&&check(fath,v,d[v]+1))
ans=min(ans,v);
for(int i=first[v];i;i=ed[i].next)
{
int u=ed[i].e;
if(i==fath) continue;
if(check(fath,i,d[v]+1))
dfs1(u,i^1);
}
}
bool dfs2(int v,int fath,int p)
{
if(v==p)
{
merge(fath,v);
return true;
}
for(int i=first[v];i;i=ed[i].next)
{
int u=ed[i].e;
if(i==fath) continue;
if(dfs2(u,i^1,p))
{
merge(fath,i);
return true;
}
}
return false;
}
int main()
{
int T,n;
cin>>T;
while(T--)
{
cin>>n;
memset(first,0,sizeof(first));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
memset(ed,0,sizeof(ed));
memset(d,0,sizeof(d));
en=(n+1)/2*2+1;
for(int i=1;i<=n;i++)
cin>>p[i];
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
d[x]++,d[y]++;
add_edge(x,y);
add_edge(y,x);
}
for(int i=1;i<=en;i++)
fa[i]=i,size[i]=1;
for(int i=1;i<=n;i++)
{
int v=p[i];
ans=n+1;
dfs1(v,v);
dfs2(v,v,ans);
cout<<ans<<" ";
}
cout<<'\n';
}
return 0;
}

Day2

T1 Emiya家今天的饭

思路

动态规划

我们先不考虑每列不超过一半的限制,求出总的方案数量,然后再减去考虑这个限制之后不合法的方案数目

所以我们问题现在就变成了求任意列所选的结点超过所有选的结点的一般的方案数目

肯定只有一列的超过总结点的一半,所以我们枚举这个超过的列,然后DP

f[i][j][k]f[i][j][k] 表示前 ii 行选 jj 个结点,当前列选了 kk 个结点的方案数量

然后发现,我们实际上只是想知道 j,kj,k 的大小关系,所以我们把 j,kj,k 合并到一维

我们就要限制 k>j2k > \lfloor\dfrac j 2 \rfloor ,然后 2k+nj>n2k+n-j>n

我们观察发现 njn-j 就是 没有选的行

对于每个结点,如果选了,那么这个行就选了两次

如果这个行没有选的话,所有列选了一次

所以我们要找的就是当前列被选超过 nn 次的方案

所以就有
f[j][k]=(f[j][k]+f[j1][k](cnt[j]w[j][i]))f[j][k+1]=(f[j][k+1]+f[j1][k])f[j][k+2]=(f[j][k+2]+f[j1][k]w[j][i]) f[j][k]=(f[j][k]+f[j-1][k]*(cnt[j]-w[j][i])) \\ f[j][k+1]=(f[j][k+1]+f[j-1][k]) \\ f[j][k+2]=(f[j][k+2]+f[j-1][k]*w[j][i])
在计算的时候都模一个 998244353998244353 就好了

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int maxn=205;
const int maxm=3005;
const int mod=998244353;
int n,m;
int ans=1;
int cnt[maxn],a[maxn][maxm],f[maxn][maxm];
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
cnt[i]=(cnt[i]+a[i][j])%mod;
}
ans=(ans*(cnt[i]+1))%mod;
}
ans=(ans+mod-1)%mod;
for(int i=1;i<=m;i++)
{
memset(f,0,sizeof(f));
f[0][0]=1;
for(int j=1;j<=n;j++)
{
for(int k=0;k<=2*(j-1);k++)
{
f[j][k]=(f[j][k]+f[j-1][k]*(cnt[j]-a[j][i]))%mod;
f[j][k+1]=(f[j][k+1]+f[j-1][k])%mod;
f[j][k+2]=(f[j][k+2]+f[j-1][k]*a[j][i])%mod;
}
}
for(int j=n+1;j<=2*n;j++)
ans=(ans+mod-f[n][j])%mod;
}
cout<<ans<<'\n';
return 0;
}

T2 划分

思路

简化一下题意

就是对于一个长度为 nn 的序列 {ai}\{a_i\} ,我们将这个序列划分成若干个部分,使得这个序列的每个部分的和从左向右递增,且它们和的平方的和最小

我们观察可以发现,分成的序列越多越好,最后一个越小越好

因为
(x+y)2x2+y2 (x+y)^2 \ge x^2+y^2
所以我们考虑动态规划

我们设 f[i][j]f[i][j] 表示划分到第 ii 个段,前驱为 jj 的最小时间

则有
f[i][j]=f[j][k]+(sum[i]sum[j])2 f[i][j]=f[j][k]+(sum[i]-sum[j])^2
其中 sum[i]sum[i] 可以前缀和预处理一下

然后我们考虑优化一下,如果 jj 固定不动的话,我们可以发现在移动 ii 的过程中 kk 也在移动,满足一个单调的性质,我们对 f[j][k]f[j][k] 维护一个最小值的单调序列

然后接着,我们可以发现一个结论

f[i][j]f[i][j1]f[i][j] \le f[i][j-1] 在合法的时候是恒成立的

所以我们最右边的前驱是最优的,所以我们不考虑更新,我们考虑转移的点

维护一个单调序列,最左边的前驱,因为前缀和也是单调的,所以当左边队首加一的时候移动左端点,右端点维护单调递增的区间段,如果有了赚一点之后,我们就能直接算答案了

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int maxn=40000005;
const int maxm=100005;
const int mod=(1<<30);
int n,type;
int x,y,z,m;
int a[maxn],b[maxn];
int l[maxm],r[maxm],p[maxm];
int q[maxn],pre[maxn];
long long sum[maxn];
inline long long d(int x)
{
return sum[x]-sum[pre[x]];
}
signed main()
{
cin>>n>>type;
if(type==1)
{
cin>>x>>y>>z;
cin>>b[1]>>b[2]>>m;
for(int i=1;i<=m;i++)
cin>>p[i]>>l[i]>>r[i];
for(int i=3;i<=n;i++)
b[i]=(1ll*b[i-1]*x+1ll*b[i-2]*y+z)%mod;
for(int i=1;i<=m;i++)
{
for(int j=p[i-1]+1;j<=p[i];j++)
{
a[j]=(b[j]%(r[i]-l[i]+1))+l[i];
sum[j]=sum[j-1]+a[j];
}
}
}
else
{
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
}
int head=0,tail=0;
q[0]=0;
for(int i=1;i<=n;i++)
{
while(head<tail&&d(q[head+1])+sum[q[head+1]]<=sum[i])
head++;
pre[i]=q[head];
while(head<tail&&d(q[tail])+sum[q[tail]]>=d(i)+sum[i])
tail--;
q[++tail]=i;
}
int now=n;
__int128 ans=0,tmp=1;
while(now)
{
tmp=d(now);
tmp*=d(now);
ans+=tmp;
now=pre[now];
}
int st[50];
int tp=0;
while(ans)
{
st[++tp]=ans%10;
ans/=10;
}
while(tp)
cout<<st[tp--];
return 0;
}

T3 树的重心

思路

巨难的题目

就来赏析一下吧

摘自@xht

考虑每个点作为重心的次数

首先拿出一个重心当做根 rootroot

对于一个不是 rootroot 的点 xx ,如果 xx 是鸽掉的某跳边的重心,这条边一定不在 xx 的子树之内

设鸽掉一条边之后 ,另外一棵树的大小为 SS

gx=maxgson(x){sy}g_x=max_{g\in son(x)}\{s_y\} 也就是 xx 的子树内最大的一个子树

因为 xx 是重心,所以根据定义,这个点的最大子树不超过这个子树大小的一半,有:
2×(nSsx)nS2×gxnS 2 \times (n-S-s_x) \le n-S \\ 2 \times g_x \le n-S

n2sxSn2gx n-2s_x \le S \le n-2g_x
所以我们要找到一个 xx ,问有多少条割掉的边满足:

  1. n2sxSn2gxn-2s_x \le S \le n-2g_x
  2. 边不在 xx 的子树内

如果只有第一个条件,可以进行一次 dfsdfs ,同时拿一个树状数组动态维护割掉当前点和其父亲之间的边之后的每一个 SS 的值有多少个,那么对于每一个点的询问实际上就是一区间求和

然后我们要去掉不满足第二个条件,即在 xx 的子树内的边,可以再拿一个树状数组按照 dfsdfs 序动态维护经过所有的点的每一个 ss 值有几个,然后我们对于每一个点,在其子树内可以割掉的边就是回溯与进入时区间求和的差

x=rootx=root 时怎么办呢?

我们可以设 xx 的儿子中最大的结点为 uu ,第二大的结点为 vv

如果割掉的边在 uu 的子树中,则要满足:
2×svnS 2 \times s_v \le n-S
移项
Sn2sv S \le n-2s_v
如果不在,则有 :
2×sunS 2 \times s_u \le n-S
Sn2suS \le n-2s_u

都可以在 dfsdfs 的时候直接维护

这道题主要是关于树状数组的用法问题吧

1
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#include <vector>#define int long longusing namespace std;const int maxn=3e5+5;struct edge{	int e,next;}ed[maxn<<1];int en,first[maxn];void add_edge(int s,int e){	en++;	ed[en].next=first[s];	first[s]=en;	ed[en].e=e;}int n,root,s[maxn],g[maxn];int u,v,z[maxn];int ans,c1[maxn],c2[maxn];inline int lowbit(int x){	return x&(-x);}inline void add(int *c,int x,int k) {	x++;	for( ;x<=n+1;x+=lowbit(x))	c[x]+=k;}inline int ask(int *c,int x) {	x++;	int res=0;	for( ;x;x-=lowbit(x))	res+=c[x];	return res;}void dfs1(int x, int fa) {	s[x]=1,g[x]=0;	bool flag=true;	for(int i=first[x];i;i=ed[i].next) 	{		int y=ed[i].e;		if(y==fa) continue;		dfs1(y,x);		s[x]+=s[y];		g[x]=max(g[x],s[y]);			if(s[y]>(n>>1)) 		flag=0;	}	if(n-s[x]>(n>>1)) 	flag=false;	if(flag) 	root=x;}void dfs2(int x,int fa) {	add(c1,s[fa],-1);	add(c1,n-s[x],1);	if (x^root) 	{		ans+=x*ask(c1,n-2*g[x]);		ans-=x*ask(c1,n-2*s[x]-1);		ans+=x*ask(c2,n-2*g[x]);		ans-=x*ask(c2,n-2*s[x]-1);		if(!z[x]&&z[fa]) 		z[x]=1;		ans+=root*(s[x]<=n-2*s[z[x]?v:u]);	}	add(c2,s[x],1);	for(int i=first[x];i;i=ed[i].next) 	{		int y=ed[i].e;		if(y==fa)		continue;		dfs2(y,x);	}	add(c1,s[fa],1);	add(c1,n-s[x],-1);	if(x^root) 	{		ans-=x*ask(c2,n-2*g[x]);		ans+=x*ask(c2,n-2*s[x]-1);	}}inline void init() {	memset(ed,0,sizeof(ed));	memset(first,0,sizeof(first));	en=0;	u=v=0;	ans=0;	memset(c1,0,sizeof(c1));	memset(c2,0,sizeof(c2)); }signed main(){	int T;	cin>>T;	while(T--) 	{		init();		cin>>n;		for(int i=1;i<=n-1;i++) 		{			int x,y;			cin>>x>>y;			add_edge(x,y);			add_edge(y,x);		}		dfs1(1,0);		dfs1(root,0);		for(int i=first[root];i;i=ed[i].next) 		{			int x=ed[i].e;			if(s[x]>s[v]) 			v=x;			if(s[v]>s[u])			swap(u,v);		}		for(int i=0;i<=n;i++) 		{			add(c1,s[i],1);			z[i]=0;		}		z[u]=1;		dfs2(root,0);		cout<<ans<<'\n';	}	return 0;}

JXCSP

T1 日期

思路

不管怎么样

如果月份错了,总是可以改一个数字变成对的

如果日子错了,也是这样

所以直接就这样搞一下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
int month,day;
int main()
{
scanf("%d-%d",&month,&day);
cout<<(month>12||month==0)+(day>31||day==0||month==2&&day>28)<<endl;
return 0;
}

T2 和积和

思路

直接暴力一下40pts

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
const int MOD=1e9+7;
const int maxn=500005;
int n;
long long a[maxn],b[maxn];
long long S(long long l,long long r)
{
long long p=0,q=0;
for(int i=l;i<=r;i++)
{
p=((p%MOD)+(a[i]%MOD))%MOD;
q=((q%MOD)+(b[i]%MOD))%MOD;
}
return (1ll*(p%MOD)*(q%MOD))%MOD;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
cin>>b[i];
long long ans=0;
for(int l=1;l<=n;l++)
{
for(int r=l;r<=n;r++)
ans=(ans%MOD+S(l,r)%MOD)%MOD;
}
cout<<ans<<'\n';
return 0;
}

我们可以前缀和处理一下

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
const int MOD=1e9+7;
const int maxn=500005;
int n;
long long a[maxn],b[maxn];
long long s1[maxn],s2[maxn];
long long S(long long l,long long r)
{
long long p=0,q=0;
p=(s1[r]-s1[l-1])%MOD;
q=(s2[r]-s2[l-1])%MOD;
return (1ll*(p%MOD)*(q%MOD))%MOD;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
s1[i]=(s1[i-1]%MOD+a[i]%MOD)%MOD;
}
for(int i=1;i<=n;i++)
{
cin>>b[i];
s2[i]=(s2[i-1]%MOD+b[i]%MOD)%MOD;
}
long long ans=0;
for(int l=1;l<=n;l++)
{
for(int r=l;r<=n;r++)
ans=(ans%MOD+S(l,r)%MOD)%MOD;
}
cout<<ans<<'\n';
return 0;
}

然后再把式子变形一下子

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <string>
#include <cstring>
using namespace std;
const int MOD=1e9+7;
const int maxn=500005;
int n;
long long a[maxn],b[maxn];
long long s1[maxn],s2[maxn],s3[maxn];
long long A[maxn],B[maxn],C[maxn];
long long S(long long l,long long r)
{
long long p=0,q=0;
p=(s1[r]-s1[l-1])%MOD;
q=(s2[r]-s2[l-1])%MOD;
return (1ll*(p%MOD)*(q%MOD))%MOD;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
s1[i]=(s1[i-1]+a[i])%MOD;
}
for(int i=1;i<=n;i++)
{
cin>>b[i];
s2[i]=(s2[i-1]+b[i])%MOD;
}
for(int i=1;i<=n;i++)
C[i]=s1[i]*s2[i]%MOD,s3[i]=(s3[i-1]+C[i]%MOD);
for(int i=1;i<=n;i++)
{
A[i]=(A[i-1]+s1[i])%MOD;
B[i]=(B[i-1]+s2[i])%MOD;
}
long long ans=0;
long long res=1;
for(int i=1;i<=n;i++)
ans=((ans+s3[n]-s3[i-1]+C[i-1]*(n-i+1)%MOD-s2[i-1]*(A[n]-A[i-1])%MOD-s1[i-1]*(B[n]-B[i-1])%MOD+MOD)%MOD+MOD)%MOD;
cout<<ans<<'\n';
return 0;
}

T3 网格图

思路

据说直接最小生成树能得到很多部分分

实际上,我们直接做最小生成树是不是有一点浪费呢?

你看,每个横线和每个竖线都会有交点,而这个就相当于最小生成树的边

我们既然是按照最小生成树来做的话

我们就直接对横线和竖线排序,这样不影响总的点的情况

然后我们剩下的就很最小生成树差不多了

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int maxn=300005;
int n,m;
int a[maxn],b[maxn];
int ans;
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=m;i++)
cin>>b[i];
sort(a+1,a+n+1);
sort(b+1,b+m+1);
//仿照kruskal排序
ans=(long long)((long long)a[1]*(m-1)+(long long)b[1]*(n-1));
int l=2,r=2;
int hang=1,lie=1;
while(l<=n&&r<=m)
{
if(a[l]<=b[r])
{
ans+=a[l]*(m-lie);
l++;
hang++;
}
else
{
ans+=b[r]*(n-hang);
r++;
lie++;
}
}
cout<<ans<<endl;
return 0;
}

T4 散步

思路

看上去对我来说是一道不可做题

我唯一的思路也就只有暴力模拟

鸽了,去复习模板去了

T5 多叉树

思路

我们如果离线的话

我们可以将一整棵树的形态弄出来,然后再直接处理

f[i]f[i] 表示以 ii 为根的子树插入数字可以有多少种方案

显然,ii 节点自身只能填 00

所以对于它的一棵子树 jj ,我们只能用剩下的 size[i]1size[i]-1 个数字中的 size[j]size[j] 个填入这个子树

所以对答案的贡献是 f[j]×Csize[i]1size[j]f[j] \times C_{size[i]-1}^{size[j]}

所以状态转移方程是
f[i]=json(i)(f[j]×Csize[i]1size[j]) f[i]= \prod_{j \in son(i)} (f[j] \times C_{size[i]-1}^{size[j]})
如果是强制在线的话,我们在连边的时候吧贡献直接乘到 f[i]f[i] 里面就好了

然后用并查集处理,预处理阶乘求组合数,快速幂求逆元即可

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
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <queue>
#define int long long
using namespace std;
const int mod=1000000007;
const int maxn=300005;
int n,m,ans;
int fa[maxn];
int fac[maxn],f[maxn],size[maxn];
int get(int x)
{
return fa[x]=(x==fa[x]? x:get(fa[x]));
}
int ksm(int a,int b)
{
int res=1;
while(b)
{
if(b&1)
res=1ll*res%mod*a%mod;
a=1ll*a%mod*a%mod;
b>>=1;
}
return res%mod;
}
void init()
{
for(int i=1;i<=n;i++)
{
f[i]=size[i]=1;
fa[i]=i;
}
fac[0]=fac[1]=1;
for(int i=2;i<=n;i++)
fac[i]=(fac[i-1]*i)%mod;
}
int C(int x,int y)
{
return fac[x]*ksm(fac[y],mod-2)%mod*ksm(fac[x-y],mod-2)%mod;
}
void merge(int x,int y)
{
int f1=get(x),f2=get(y);
f[f2]=(f[f2]*C(size[f1]+size[f2]-1,size[f1])%mod*f[f1])%mod;
fa[f1]=f2,size[f2]+=size[f1];
}
signed main()
{
cin>>n>>m;
init();
while(m--)
{
int op;
cin>>op;
if(op==1)
{
int x,y;
cin>>x>>y;
x=(x+ans)%n+1;
y=(y+ans)%n+1;
merge(x,y);
}
else
{
int x;
cin>>x;
x=(x+ans)%n+1;
ans=f[get(x)];
cout<<ans<<'\n';
}
}
return 0;
}