你最愿意做的哪件事,才是你的天赋所在

0%

2020牛客暑期多校训练营(第二场)补题记录

题目

E

题意

就是给长度为1e5的大小为[0,$2^{18}$)的一个序列,要你求任意1~n个数异或的最大值,首先很容易发现当i=18的时候肯定已经是最大了,就是任意18个数异或已经是最大了,因为范围是[0,2^18)很显然,我1~18个数肯定会异或出尽量多的1,然后之后就是$a[i+2]=a[i]$即可,但是前18个我们怎么求呢,我们可以用卷积,只不过这里是异或卷积。暴力求卷积的话复杂度是$O(n^2logn)$的,所以用FWT快速沃尔什变换来写,与FFT的思想是差不多的,都是分治。不过重新定义了运算,这样就能$O(nlog^2n)$的复杂度解决了,总复杂度就是$O(n+wlog^2w)$

代码实现
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
111
112
#include<bits/stdc++.h>
using namespace std;
const int P = 998244353;

void add(int &x, int y) {
(x += y) >= P && (x -= P);
}
void sub(int &x, int y) {
(x -= y) < 0 && (x += P);
}
struct FWT {
int extend(int n) {
int N = 1;
for (; N < n; N <<= 1);
return N;
}
void FWTor(std::vector<int> &a, bool rev) {
int n = a.size();
for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
if (!rev) add(a[i + j + m], a[i + j]);
else sub(a[i + j + m], a[i + j]);
}
}
}
void FWTand(std::vector<int> &a, bool rev) {
int n = a.size();
for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
if (!rev) add(a[i + j], a[i + j + m]);
else sub(a[i + j], a[i + j + m]);
}
}
}
void FWTxor(std::vector<int> &a, bool rev) {
int n = a.size(), inv2 = (P + 1) >> 1;
for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
int x = a[i + j], y = a[i + j + m];
if (!rev) {
a[i + j] = (x + y) % P;
a[i + j + m] = (x - y + P) % P;
} else {
a[i + j] = 1LL * (x + y) * inv2 % P;
a[i + j + m] = 1LL * (x - y + P) * inv2 % P;
}
}
}
}
std::vector<int> Or(std::vector<int> a1, std::vector<int> a2) {
int n = std::max(a1.size(), a2.size()), N = extend(n);
a1.resize(N), FWTor(a1, false);
a2.resize(N), FWTor(a2, false);
std::vector<int> A(N);
for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
FWTor(A, true);
return A;
}
std::vector<int> And(std::vector<int> a1, std::vector<int> a2) {
int n = std::max(a1.size(), a2.size()), N = extend(n);
a1.resize(N), FWTand(a1, false);
a2.resize(N), FWTand(a2, false);
std::vector<int> A(N);
for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
FWTand(A, true);
return A;
}
std::vector<int> Xor(std::vector<int> a1, std::vector<int> a2) {
int n = std::max(a1.size(), a2.size()), N = extend(n);
a1.resize(N), FWTxor(a1, false);
a2.resize(N), FWTxor(a2, false);
std::vector<int> A(N);
for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
FWTxor(A, true);
return A;
}
} fwt;
const int N = 1<<18;

int main() {
int n;
scanf("%d", &n);
vector<int>ans(n);
std::vector<int> a1(N), a2(N);
int mx = 0;
for (int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
mx=max(mx,x);
a1[x]++;
a2[x]++;
}
ans[0]=mx;
int cnt = 1;
std::vector<int> A;
for(int j=1;j<min(21,n);j++) {
A = fwt.Xor(a1, a2);
for(int i=N-1;i>=0;i--)
if(A[i]){
ans[cnt++]=i;
break;
}
for(int i=0;i<N;i++)a1[i]=A[i];
}
for(int i=2;i<n;i++){
ans[i]=max(ans[i-2],ans[i]);
}
for(int i=0;i<n;i++){
printf("%d ",ans[i]);
}
return 0;
}

B

题意

给n个点,求一个过原点的圆,使得这个圆上的点数最多。

思路

三点确定一个圆,已经有原点了,再需要两个点,所以暴力枚举两个点,然后可以得到n^2个圆心,然后对这n^2个圆心排序去重,重复的个数为num,则答案为$C_{ans}^2 = num$,因为任意选圆上的两点都可以这个圆心。

代码实现
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
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
#define INF 0x3f3f3f3f
#define PI acos(-1)
long double x[2005],y[2005];
struct point
{
long double x0,y0;
};
long double a,b,c,d,e,f,kkx,kky,kx,ky;
int maxn,kcnt,cnt;
vector<point>p;
point work;
void init(int k1,int k2)
{
if((-y[k1])*(x[k2]-x[k1])-(y[k2]-y[k1])*(-x[k1])!=0)
{
a=2*(x[k2]-x[k1]);
b=2*(y[k2]-y[k1]);
c=x[k2]*x[k2]+y[k2]*y[k2]-x[k1]*x[k1]-y[k1]*y[k1];
d=2*(-x[k2]);
e=2*(-y[k2]);
f=-x[k2]*x[k2]-y[k2]*y[k2];
work.x0=(b*f-e*c)/(b*d-e*a);
work.y0=(d*c-a*f)/(b*d-e*a);
p.push_back(work);
//cout<<kx<<" "<<ky<<endl;
}
}
int cmp(point ka,point kb)
{
if(ka.x0==kb.x0) return ka.y0<kb.y0;
else return ka.x0<kb.x0;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int n;
maxn=0;
cnt=0;
kcnt=0;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>x[i]>>y[i];
}
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
init(i,j);
sort(p.begin(),p.end(),cmp);
for(int i=0;i<p.size();i++)
{
if(kcnt==0)
{
kcnt=1;
kx=p[i].x0;
ky=p[i].y0;
}
else
{
if(p[i].x0==kx&&p[i].y0==ky) kcnt++;
else
{
if(kcnt>maxn)
{
maxn=kcnt;
kkx=kx;
kky=ky;
}
kcnt=1;
kx=p[i].x0;
ky=p[i].y0;
}
}
}
int sum;
if(kcnt>maxn)
{
kkx=kx;
kky=ky;
maxn=kcnt;
}
cnt=1;
while(cnt*(cnt-1)/2!=maxn)cnt++;
if(cnt==0) cout<<"1"<<endl;
else cout<<cnt<<endl;
return 0;
}

C

题意

给出一棵数,找出最少链覆盖整棵树的边

思路

图论的知识欠缺,只知道有个结论,并且数据好像还水了。

正解应该是先找重心,这里重心的定义是以叶子节点为个数来找,跟普通的找中心方式不同,然后根据重心dfs找出dfs序,叶子节点根据dfs序来排序最后得到一个序列,答案就是1~n/2+1,2~n/2+2这样配对即可。

同校奆用了一种较为繁琐但是我感觉是正确的方法,对一个节点的子节点,如果子节点的叶子节点个数为偶数个,那么保留2个,如果为奇数个,那么保留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
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
int const N=2e5+10;
int const INF=2e6;
vector<int>f[N];
int minl=INF,ans,n,tot=0,father[N],sz[N],dp[N],num[N];
void dfs(int u,int fa){
sz[u]=1;
for (int i=0;i<f[u].size();i++){
int j=f[u][i];
if (j==fa) continue;
dfs(j,u);
sz[u]+=sz[j];
dp[u]=max(dp[u],sz[j]);
}
dp[u]=max(dp[u],n-sz[u]);
if (minl>dp[u]){
minl=dp[u];
ans=u;
}
}
void dfs2(int u,int fa){
if (f[u].size()==1) num[++tot]=u;
father[u]=fa;
for (int i=0;i<f[u].size();i++){
int j=f[u][i];
if (j==fa) continue;
dfs2(j,u);
}
}
int main(){
scanf("%d",&n);
if (n==1){
printf("0\n");
return 0;
}
int x,y;
for (int i=1;i<n;i++){
scanf("%d%d",&x,&y);
f[x].push_back(y);
f[y].push_back(x);
}
//dfs(1,0);
dfs2(1,0);
printf("%d\n",(tot+1)/2);
for (int i=1;i<=(tot+1)/2;i++)
printf("%d %d\n",num[i],num[tot/2+i]);
return 0;
}

F

题意

给一个矩阵,$martix_ij=lcm(i,j)$,然后给一个k,要你求k*k的子矩阵的最大值之和,也就是

$\sum_{i=k}^{n}\sum_{j-k}^{m}max(martix_{[i-k+1\dots i][j-k+1\dots i]})$

正解感觉应该是单调队列(但是我不会),但是根据lcm的性质可以判断出来的是已当前点(x,y)为右下角的子矩阵的最大值应该分布在

(x-5~x,y-5~y)这个区间(不会证明),大概就是如果x与y互质,在判断x与y-1是否互质,大概想一下感觉可行···。然后就确定一个区间暴力冲就好了。打表的时候要根据对称性,不然会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
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define vi vector<int>
#define vl vector<vll>
#define pii pair<int,int>
#define pll pair<ll>
inline bool isprime(ll num)
{if(num==2||num==3)return true;
if(num%6!=1&&num%6!=5)return false;
for(int i=5;1ll*i*i<=num;i+=6){if(num%i==0||num%(i+2)==0)return false;}
return true;}
const int mod = 1e9+7;
inline ll mul(ll a,ll b,ll c){return (a*b-(ll)((ld)a*b/c)*c+c)%c;}
inline ll exgcd(ll a,ll b,ll &x,ll &y){if(!b){x=1;y=0;return a;}ll g = exgcd(b,a%b,y,x);y-=a/b*x;return g;}
inline ll quick_pow(ll a,ll b,ll mod){ll res=1;while(b){if(b&1)res=mul(res,a,mod);a=mul(a,a,mod);b>>=1;}return res;}
inline ll quick_pow(ll a,ll b){ll res=1;while(b){if(b&1)res=mul(res,a,mod);a=mul(a,a,mod);b>>=1;}return res;}
inline ll inv(ll x){return quick_pow(x,mod-2);}
inline ll inv(ll x,ll mod){return quick_pow(x,mod-2,mod);}
inline ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
inline ll lcm(ll a,ll b){return a*b/gcd(a,b);}
const int N = 5e3+10;
int a[N][N];
bool vis[N][N];
int main(){
int n,m,k;
scanf("%d %d %d",&n,&m,&k);
for(int i=1;i<=5000;i++){
for(int j=i;j<=5000;j++){
a[i][j]=lcm(i,j);
a[j][i]=a[i][j];
}
}
ll ans = 0;
for(int i=k;i<=n;i++){
for(int j=k;j<=m;j++){
if(k==1){
ans+=a[i][j];
continue;
}
if(a[i][j]==i*j)ans+=a[i][j];
else{
ll mm = 0;
for(int x=i-min(k,10)+1;x<=i;x++){
for(int y=j-min(k,10)+1;y<=j;y++){
mm = max(1ll*a[x][y],mm);
}
}
ans+=mm;
}

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

J

题意

给一个序列{A},问从{1,2,3,4,5,6,….,n}这个序列经过P的置换规则恰巧k次得到A,求这个P序列的置换规则。

再直白点就是P序列的意义是得到一个规则a[i]=a[p[i]]这样不断的置换a一开始为{1,2,3,4,5,7…..n}

这道题可以说是群论的知识(离散没学好,雾),首先置换其实就是几个环再不停的挪动,例如有一个置换规则P{2,3,1},{2,3,1}成一个环,{1,2,3}置换第一次

a[1]=a[2],a[2]=a[3],a[3]=a[1] => {2,3,1} 实际上就是把1,2,3的1放到了末尾,也就是把第一个放到了最后一个去,也就是整体左移了。

很显然,如果置换size(环的大小)次,那么环就会恢复原状了,

就是说,只有(0,size)的置换是有效的,但问题是我们要置换k次得到A。

思路

置换k次得到A,然后置换size次为原本的,我们可以把每个环看成一个模群,即%size的一个群,然后求k在该模群下的逆元,也就是

$k^{-1}$,A置换$k^{-1}$次就是P,因为$p\times k=A所以A\times k^{-1}=p$,正向置换是环向左移位,所以反向就是环向右移位。

代码实现
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<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define vi vector<int>
#define vl vector<vll>
#define pii pair<int,int>
#define pll pair<ll>
inline bool isprime(ll num)
{if(num==2||num==3)return true;
if(num%6!=1&&num%6!=5)return false;
for(int i=5;1ll*i*i<=num;i+=6){if(num%i==0||num%(i+2)==0)return false;}
return true;}
int mod;
inline ll mul(ll a,ll b,ll c){return (a*b-(ll)((ld)a*b/c)*c+c)%c;}
inline ll exgcd(ll a,ll b,ll &x,ll &y){if(!b){x=1;y=0;return a;}ll g = exgcd(b,a%b,y,x);y-=a/b*x;return g;}
inline ll quick_pow(ll a,ll b,ll mod){ll res=1;while(b){if(b&1)res=mul(res,a,mod);a=mul(a,a,mod);b>>=1;}return res;}
inline ll quick_pow(ll a,ll b){ll res=1;while(b){if(b&1)res=mul(res,a,mod);a=mul(a,a,mod);b>>=1;}return res;}
inline ll inv(ll x){return quick_pow(x,mod-2);}
inline ll inv(ll x,ll mod){return quick_pow(x,mod-2,mod);}
inline ll gcd(ll a,ll b){return b?gcd(b,a%b):a;}
inline ll lcm(ll a,ll b){return a*b/gcd(a,b);}
const int N = 2e5+10;
vector<vector<int>>arc;
int a[N];
int b[N];
bool vis[N];
vector<int>tmp;
void getarc(int s){
while(!vis[s]){
vis[s]=1;
tmp.push_back(s);
s=a[s];
}
}
int main(){
ll n,k;
scanf("%lld %lld",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++){
if(!vis[a[i]]){
if(a[i]==i){
vis[a[i]]=1;
b[i]=i;
continue;
}
getarc(a[i]);
arc.push_back(tmp);
tmp.clear();
}
}
for(int i=0;i<arc.size();i++){
int len = arc[i].size();
mod=len;
ll in,in1;
exgcd(k,len,in,in1);
in=(in+len)%len;
for(int j=0;j<arc[i].size();j++){
b[arc[i][j]]=arc[i][(j+in)%mod];
}
}
for(int i=1;i<=n;i++){
printf("%d ",b[i]);
}
}

G

题意

给两个数组,一个长度为n的A,一个长度为m的B,询问在A上有多少长度为m的区间都满足$A_i>=B_i$

思路

用m的数据范围有40000,想到用bitset,每个$A_i$元素对应一个长度为m的bitset,如果$A_i>B_j$那么$bitset_i[j]=1$,例如样例中的

6 3
1 4 2 8 5 7
2 3 3

1-> 000

4->111

2->100

如果暴力遍历O(n)求bitset显然超时,但是注意到对应的bitset只有m个不同的。如果把这m个不同的bitset处理出来,每个$A_i$对应的bitset用$logm$的复杂度来找。

如何处理处m个不同的bitset?我们对B数组从小到大排序,然后遍历B,对当前遍历到的位置进行置为1即可。

然后我们再来看问题,设n个长度为m的bitset,记为$cur_i$,$cur[j]=1$的含义是当且仅当$\forall k\in[j,m]\quad A_{i+k-j}>=B_k$ ,根据这个定义就可以退出一个状态转移

$I_m$表示m位为1的bitset,这是为了保证只要$A_i>=B_i 那么cur[m]=1$,因为右移位的时候把cur[m]置为0了。

当cur[1]=1的时候答案就+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
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 150010;
const int M = 4e4+10;
const int INF = 1e9+7;
bitset<M>s[M];
bitset<M>cur;
pair<int,int> a[N],b[M];
int main(){
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i].first);
a[i].second=i;
}
for(int i=1;i<=m;i++){
scanf("%d",&b[i].first);
b[i].second=i;
}
sort(b+1,b+1+m);
b[m+1].first=INF;
b[m+1].second=m+1;
for(int i=1;i<=m;i++){
s[i]=s[i-1];
s[i].set(b[i].second);
}
bitset<M>I;
I.set(m);
int ans = 0;
for(int i=n;i>=1;--i){
int pos = upper_bound(b+1,b+1+m,a[i])-b-1;
cur = (cur>>1|I)&s[pos];
if(cur[1]==1)ans++;
}
printf("%d\n",ans);
}
-------------你最愿意做的哪件事才是你的天赋所在-------------