2020 牛客多校第二场 ABCDFGJ

2020牛客暑期多校训练营(第二场)

A、All with Pairs

题意

给你 \(n\)个字符串\(s_1,s_2,\ldots ,s_n\) ,\(f(s,t)\)为找到最长的\(s_{1\ldots i} = t_{|t|-i+1\ldots |t|}\),就是找最长的字符串\(s\)的前缀等于字符串\(t\)的后缀,问\(\sum_{i=1}^n\sum_{j=1}^nf(s_i,s_j)^2\pmod{998244353}\)

思路

  • 判断一段字符串相等,可以把字符串转换成\(hash\),存储起来。

  • 那么就把所有后缀都转换成\(hash\)存起来,然后遍历每个前缀的贡献

  • 但是直接算当前前缀的贡献会出现重复计算贡献的情况,因为要求最长的前缀等于后缀

    比如\(abca\), 如果能在\(abca\)匹配到,那在\(a\)就一定也匹配到过,那么应该怎么去重

  • 然而\(kmp\)算法中的求\(next\)数组就可以很好的解决这个问题。

    \(i\):当前匹配到该元素的第\(i\)

    \(now\):当前的\(hash\)

    \(mp[now]\):表示\(hash\)\(now\)的后缀的个数

    \(ans = mp[now] * ( i * i - (nxt[i]-1) * (nxt[i]-1));\) 当前的贡献

代码

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll mod = 998244353;
const int maxn = 1e6+10;
const double eps = 1e-9;
const ll inf = 1e18;
const ull seed = 13331;
int T;
int n, m;
string s[maxn];
map<ull, ull> mp;	//记录后缀Hash值的个数
int nxt[maxn];		

void getNxt(string str){		//求next数组
	int len = str.length();
	for(int i = 0; i <= len; i++) 
		nxt[i] = 0;
	int i = 1, j = 0;
	nxt[0] = -1;
	while(i < len){
		if(j == -1 || str[i] == str[j]) nxt[++i] = ++j;
		else j = nxt[j];
	}
	/*for(int i = 0; i <= len; i++)
		printf("%d ", nxt[i]);
	puts("");*/
}

int main(){
	cin>>n;
	ull now, pre;
	ll ans = 0;
	for(int i =1; i <=n; i++){
		cin>>s[i];
		now = 0; pre = 1;
		for(int j = s[i].length()-1; j >= 0; j--){
			now += pre * s[i][j];	//计算后缀的Hash值
			pre *= seed;
			mp[now]++;
		}
	}
	ll tmp;
	for(int i = 1; i <= n; i++){
		getNxt(s[i]);
		now = 0;
		for(int j = 0; j < s[i].length(); j++){
			now = now * seed + s[i][j];		//当前前缀Hash值
			tmp = nxt[j+1];
			ans += mp[now] * (j+1) % mod *(j+1) % mod;	//因为从0开始遍历,贡献要j+1
			ans -= mp[now] * tmp % mod * tmp% mod;	//有mp[now]会重复计算
			ans = (ans% mod + mod)% mod;
		}
	}
	printf("%lld\n", ans);
	return 0;
}


B、Boundary

题意

给你\(n\)个点,找出一个圆,使得\((0, 0)\)要在圆上,并且使得最多的点在圆上,输出在圆上的点的个数。(给的点不会重复且没有原点)
(不知道是不是代码的问题,改了很多版本都差不多,但是精度一直在出问题)

思路

(1)同心圆的所对的夹角相等,那么固定一个点\(P\)去枚举其它点\(A\),求出出现最多\(\angle{OAP}\)的个数+1,则为答案。

(2)任取两个点\(A,B\)求出\(\triangle{OAB}\)的外接圆的圆心,排序,找出出现最多次数的圆心次数\(ans\)。设有\(x\)个点在圆上 , 则\(C_x^2 = ans\)

代码

只写了方案一

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int maxn = 1e6+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, m;
double x[maxn], y[maxn];
vector<double> vec;
int main(){
	scanf("%d", &n);
	for(int i =1; i <= n; i++){
		scanf("%lf%lf", &x[i], &y[i]);
	}
	int ans = 1;
	for(int i = 1; i <= n; i++){
		vec.clear();
		for(int j = 1 ; j <= n; j++){
			if(x[i]*y[j] - x[j]*y[i] >= 0) continue;
			double a = sqrt(x[i] * x[i] + y[i] * y[i]);
			double b = sqrt((x[i]-x[j])*(x[i]-x[j]) + (y[i]-y[j])*(y[i]-y[j]));
			double c = sqrt(x[j] * x[j] + y[j] * y[j]);
			double A = (b*b+c*c-a*a)/(2.0*b*c);
			vec.pb(A);
		}
		sort(vec.begin(), vec.end());
		int num = 1, len = vec.size();
		for(int i = 1; i < len; i++){
			if(abs(vec[i] - vec[i-1]) < 1e-13)		//精度要设置大一些
				num++;
			else
				num = 1;
			ans = max(ans, num+1);
		}
        if(len) ans = max(ans, 2);
	}
	printf("%d\n", ans);
	return 0;
}

C、Cover the Tree

题意

给你一棵树,让你选择最少的链覆盖整棵树的每条边。

思路

  • 链的两端尽可能的选叶子节点是最优的,那么最少的链数则为\((num+1)/2\)\(num\)叶子节点个数)
  • 根据\(dfs\)序遍历,把叶子节点存入数组,把前\(\frac{2}{num}\)个叶子节点和后\(\frac{2}{num}\)个叶子节点一一匹配。
  • 具体证明,个人觉得可以贪心的思想来看,就是尽量把相邻的分开,那么就能够尽量的遍历到父节点之间的边。

代码

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int maxn = 1e6+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, m;
int in[maxn];
vector<int> e[maxn], vec;
void dfs(int u, int fa){
    int flag = 0;
    for(auto v: e[u]){
        if(v == fa) continue;
        flag = 1;
        dfs(v, u);
    }
    if(!flag) vec.pb(u);
}
int main(){
    int x, y;
    scanf("%d", &n);
    for(int i = 1; i < n; i++){
        scanf("%d%d", &x, &y);
        e[x].pb(y);
        e[y].pb(x);
        in[x]++; in[y]++;
    }
    if(n == 2){
        printf("1\n1 2\n");
        return 0;
    }
    for(int i = 1; i<= n; i++){
        if(in[i] > 1){		//找一个非叶子节点为根
            dfs(i, i);
            break;
        }
    }
    int ans = vec.size();
    printf("%d\n", (ans+1)/2);
    int tmp = ans/2;
    for(int i = 0; i < (ans+1)/2; i++)
        printf("%d %d\n", vec[i], vec[i+tmp]);
    return 0;
}

D、Duration

题意

给你一天之内的两个时间,问两个时间相差多少秒

思路

两个时间转换成秒,相减取\(abs\)

代码

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int maxn = 1e6+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, m;
int a, b, c, d, e, f;
int main(){
	scanf("%d:%d:%d%d:%d:%d", &a, &b, &c,&d, &e, &f);
	int x = c + b * 60 + a * 60 * 60;
	int y = f + e * 60 + d * 60 * 60;
	printf("%d\n", abs(y-x));
	
	return 0;
}

F、Fake Maxpooling

题意

给你一个\(n*m\)的矩阵,\(a[i][j] = lcm(i, j)\), 问每个\(k*k\)的子矩阵最大元素的和为多少。

思路

(1)\(O(nm)\)

  • 类似素数筛的方法去给数组赋值
  • 单调队列维护最大值

(2)\(O(nm\log{nm})\)

  • 可以用库里\(\ \_\_gcd\) 函数, (感觉比手写快很多)
  • 可以用类似\(RMQ\)二维维护最大值
    以上(2)二选其一可以过的,两个都用我就不知道了,(说不定扣扣就能过)。

代码

赛场中写的,单调队列写的又麻烦又慢,可以看大佬的代码(大佬的代码非常优雅)

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int maxn = 5e3+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, m, k;

int a[maxn][maxn], q[maxn][maxn], Q[maxn], l[maxn], r[maxn], L, R;
//q[i]维护每行当前从大到小,且区间符合的单调队列

void add(int x, int y){		//添加第i行第j个元素
	while(l[x] <= r[x] && a[x][q[x][r[x]]] <= a[x][y]) r[x]--;
	while(l[x] <= r[x] && q[x][l[x]] < y-k+1) l[x]++;
	r[x]++;
	q[x][r[x]] = y;
}

int main(){
	scanf("%d%d%d",&n, &m, &k);
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){	//手写gcd,场上超时了
			a[i][j] = i*j/__gcd(i, j);
		}
		l[i] = 1;
	}
	for(int i = 1; i <= n; i++){
		for(int j = 1; j < k; j++){
			add(i, j);
		}
	}
	ll ans = 0;
	for(int j = k; j <= m; j++){
		L = 1; R = 0;
		for(int i = 1; i < k; i++){
			add(i, j);
			while(L <= R && a[Q[R]][q[Q[R]][l[Q[R]]]] <= a[i][q[i][l[i]]]) R--;
			R++; Q[R] = i;
		}
		for(int i = k; i <= n; i++){
			add(i, j);
			while(L <= R && a[Q[R]][q[Q[R]][l[Q[R]]]] <= a[i][q[i][l[i]]]) R--;
			while(L <= R && Q[L] < i-k+1) L++;
			R++; Q[R] = i;
			ans += a[Q[L]][q[Q[L]][l[Q[L]]]]*1ll;
		}
	}
	printf("%lld\n", ans);
}

G、Greater and Greater

题意

给你一个长度分别为\(n, m\)的数组\(A, B\), 问数组\(A\)存在多少个长度为\(m\)的子区间\(S\),使得\(\forall i \in \{1, 2\ldots, m\} \ S_i > B_i\)

思路

(有点讲不清楚,主要是讲代码)

  • 排序\(B\)数组
  • \(bitset\)数组\(bs[i]\)记录\(B\)数组前\(i\)小的位置
bitset<mx> bs[mx];
bool cmp(int x, int y){
	return B[x] < B[y];
}

for(int i = 1; i <= m; i++) id[i] = i;
sort(id+1, id+m+1, cmp);	//id根据b数组排序
for(int i = 1; i <= m; i++){	
	bs[i] = bs[i-1];
	bs[i].set(id[i]);		//bs为1的位置,就是前i小的元素
}
  • 通过二分查找每个\(A[i]\) 有哪些\(B[j]\)小于自己
int get(int x){
	int l = 1, r = m, mid, ans = 0;
	while(l <= r){
		mid = l+r>>1;
		if(x >= B[id[mid]]){
			ans = mid;
			l = mid+1;
		}
		else
			r = mid-1;
	}
	return ans;
}

bs[get(A[i])]	//bs中为1的位置的B元素小于A[i]
  • 用一个\(res\)记录, \(res[j]\)表示当前\(\forall k \in [j, m], A[i+k - 1] >B[k]\), 如果\(res[1] = 1\),则表示整个区间都符合
bitset<mx> res;
int ans = 0;
for(int i = n; i >= 1; i--){	//从后往前遍历
	res >>= 1;	//把前面符合往后移
	res &= bs[get(a[i])];	// bs[get(a[i])] 中有为0的位置,那么在a[i]在这个位置则不符合
	if(a[i] >= b[m]) res.set(m); //当前位大于,则直接赋值
	ans += res[1];
}

通过\(\&\)运算,只要中间出现0,那么这个位置之后肯定不行,手动模拟一下就比较清楚了。

代码

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
#define fi first
#define se second 
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int maxn = 2e5+10;
const int mx = 4e4+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, m;
int a[maxn], b[maxn], id[maxn];
bitset<mx> bs[mx], res;
bool cmp(int x, int y){
	return b[x] < b[y];
}

int get(int x){
	int l = 1, r = m, mid, ans = 0;
	while(l <= r){
		mid = l+r>>1;
		if(x >= b[id[mid]]){
			ans = mid;
			l = mid+1;
		}
		else
			r = mid-1;
	}
	return ans;
}
int main(){
	scanf("%d%d", &n,&m);
	for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
	for(int i = 1; i <= m; i++) scanf("%d", &b[i]), id[i] = i;
	sort(id+1, id+m+1, cmp);
	for(int i = 1; i <= m; i++){
		bs[i] = bs[i-1];
		bs[i].set(id[i]);
	}
	int ans = 0;
	for(int i = n; i >= 1; i--){
		res >>= 1;
		res &= bs[get(a[i])];
		if(a[i] >= b[m]) res.set(m);
		ans += res[1];
	}
	printf("%d\n", ans);
	return 0;

}

J、Just Shuffle

题意

把一个排列\(\{1, 2, ..., n\}\) 经过\(k\)\(B\)置换变成数组\(A\), 现在给你数组\(A\)和次数\(k\),求排列\(\{1, 2, ..., n\}\)经过一次\(B\)置换之后的数组\(a\)

思路

(1) 我们可以把根据置换变成多个环,那么一个环同一个置换,置换\(len\)(环的长度)次为一个循环,我们知道置换\(k\)次之后的状态,那么我们找到一个\(x * k \% len = 1\)就可以求出数组\(a\)
(2)\(x = \frac{1}{k} \pmod{len}\),求x有两种方法

  • 遍历\(1\le x \le len\),找到\(x*k\%len= 1\)
  • \(x * k - y * len = 1 ,exgcd(k, len, x, y) , x = (x+len)\%len\)

代码

#include<bits/stdc++.h>
#define pb push_back
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const ll mod = 23333;
const int maxn = 2e6+10;
const double eps = 1e-9;
const ll inf = 1e18;
int T;
int n, k;

ll exgcd(ll a, ll b, ll &x, ll &y){
	if(b == 0){
		x = 1; y = 0;
		return a;
	}
	else{
		ll ans = exgcd(b, a%b, x, y);
		ll tmp = x - a/b *y;
		x = y; y = tmp;
		return ans;
	}
}

int a[maxn], b[maxn], c[maxn];
vector<int> vec;
int main(){
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
	int now;
	for(int i = 1; i <= n; i++){
		if(!b[i]){
			vec.clear();
			now = i;
			while(!b[now]){
				vec.pb(a[now]);
				b[now] = 1;
				now = a[now];
			}
			// 循环遍历
			ll len = vec.size(), inv;
			for(ll j = 1; j < len; j++){
				if(j * k % len == 1){
					inv = j; break;
				}
			}
			for(int j = 0; j < len; j++)
				c[vec[j]] = vec[(j+inv)%len];

			/* exgcd
			ll len = vec.size(), x, y;
			exgcd(k, len, x, y);
			x = (x + len)%len;
			for(int j = 0; j < len; j++)
				c[vec[j]] = vec[(x+j)%len];
			
			*/	
		}
	}
	for(int i = 1; i <= n; i++){
		printf("%d ", c[i]);
	}
	puts("");
	return 0;
}
posted @ 2020-07-15 23:24  竹攸  阅读(247)  评论(0编辑  收藏  举报