2020牛客多校第二场补题博客

一、All with Pairs(A)

题目链接:All with Pairs

题目大意: 首先给你n个字符串,定义 F ( s , t ) F(s,t) F(s,t)等于s的前缀和t的后缀的最长匹配长度,然后让你计算 ∑ i = 1 n ∑ j = 1 n f ( s i , s j ) 2 \sum_{i=1}^{n}\sum_{j=1}^{n}f(s_i,s_j)^2 i=1nj=1nf(si,sj)2,并且对结果取模。

解题思路: 首先一点,我们可以把每个字符串的每种后缀情况都hash出来,然后对于任何一个字符串来说,我们就需要看他的每一种前缀匹配了多少个后缀,把匹配长度和数量都求出来,那么这个字符串的匹配情况就求出来了。但是这样的求法存在一点问题,假设字符串s长度为1的前缀和字符串t长度为1的后缀匹配了,然后s长度为3的字符串又和字符串t长度为3的后缀匹配了,这样的话我们就重复匹配了,因为s和t只能匹配一次并且选择长度最长的。那么这里的话我们就需要减掉长度为1的那种匹配,怎么减是关键。我们举几个例子:s的前三个字符为aba,t的后三个字符为aba。首先我们把t后缀情况hash出来,也就是a,ba,aba。s的前缀信息是a,ab,aba。也就是说a和a匹配了,并且aba和aba匹配了;s的前五个字符为abcab,t的后五个字符为abcab。那么ab和ab匹配,abcab和abcab匹配,这里ab和ab匹配是不需要的,也就是说我们需要将ab匹配的情况的数量减掉abcab匹配的情况的数量。处理好这里的细节后就比较简单了,还需要注意的是hash的转换最好用一个较大的质数来表示进制。(具体细节看代码)
代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull; 
const ull mod=998244353;
string S[100005];
map<ull,int> mp;
int Count[1000005]; //count[i]表示 长度为i+1的前缀有多少个匹配的后缀 
int nt[1000005]; //nt[i] 表示前i个字符串长前缀后缀匹配长度 
ull base=233;  //进制 
void kmp_nt(string p) //求出p字符串的最长前缀后缀匹配长度 
 {
     nt[0]=0;
     for(int i=1,j=0;i<p.size();i++){
         while(p[i]!=p[j]&&j) j=nt[j-1];
         if(p[i]==p[j]) j++;
         nt[i]=j;
     }
 }
int main(){
	int n;
	ull ans=0;
	scanf("%d",&n);
	for(int i=0;i<n;i++)
	{
		cin>>S[i];
		ull t=0,p=1;
    	for(int j=S[i].size()-1;j>=0;j--){ //把每一个字符串的后缀都hash一遍 
         t+=p*(S[i][j]-'a'+1);
         p*=base;
         mp[t]++;  //记录数量 
     }
	}
	ull t;
	for(int i=0;i<n;i++)
	{
		t=0;
		for(int j=0;j<S[i].size();j++) //先求出每一个前缀匹配后缀的数量 
		{
			t*=base;
			t+=S[i][j]-'a'+1;
			Count[j]=mp[t]; 
		}
		kmp_nt(S[i]);
		//减掉s和t重复的,更短的匹配的数量
		// 例如: s=abcab  t=abcab
		//那么 count[2-1]-=count[4];		 
		for(int j=0;j<S[i].size();j++) 
		{
			if(nt[j])
			Count[nt[j]-1]-=Count[j];
		}
		for(int j=0;j<S[i].size();j++) //最后将整理过的数据进行计算 
		{
			ans+=(Count[j]%mod)*(j+1)*(j+1)%mod;
			ans%=mod;
		}
	}
	cout<<ans<<endl;
	
	
	return 0; 
}


二、Boundary(B)

题目链接: Boundary
题目大意: 在一个二维平面上,给你n个点,和一个原点,问你最多有多少个点在同一个圆上,原点必须要在圆上。
解题思路: 从正面去思考可能不太好想,我们可以这样思考,在n个点枚举两个点,这两个点与原点会形成一个圆,那么就会有一个圆心,因为原点是固定的,所以圆心相同那么这个圆就是一样的,也就是说我们需要求到每次枚举的圆心,然后求出圆心的众数,通过圆心来得到点的数量。问题就变成了已知三个点求圆心坐标,这个问题就是个简单的问题了,在这三个点中去两条线段,那么这两条线段的中垂线的交点就是这三个点所在的圆的圆心了。(具体细节看代码)
代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node{
    double x,y;
}point[2005];
node xin[4000005];
  
bool cmp(node a,node b)
{
    if(abs(a.x-b.x)<1e-6)
    return a.y<b.y;
    return a.x<b.x;
}
bool equals(double a,double b)
{
    if(abs(a-b)<1e-6)
    return true;
    else
    return false;
}
int main()
{
    int n,count=0,ans=0,tmpnum=0;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        scanf("%lf%lf",&point[i].x,&point[i].y);
    }
    node A,B,tmp;
    for(int i=0;i<n;i++)
    {
        for(int j=i+1;j<n;j++)
        {
              
            A=point[i],B=point[j];
            double k1,k2,b1,b2;
            if(equals(A.y,0)&&equals(B.y,0)) //两个斜率都不存在 
            continue;
            else if(!equals(A.y,0)&&!equals(B.y,0)) //两个斜率都存在 
            {
                k1=-(A.x/A.y);
                k2=-(B.x/B.y);
                b1=(pow(A.x,2)+pow(A.y,2))/(2*A.y);
                b2=(pow(B.x,2)+pow(B.y,2))/(2*B.y);
                if(equals(k1,k2)) //斜率相等平行的情况 
                {
                    continue;
                }
                else
                {
                    tmp.x=(b2-b1)/(k1-k2);
                    tmp.y=tmp.x*k1+b1;
                    xin[count++]=tmp;
                }
            }
            else
            {
                double x;
                if(equals(A.y,0)) //A点斜率不存在 
                {
                    x=A.x/2;
                    k2=-(B.x/B.y);
                    b2=(pow(B.x,2)+pow(B.y,2))/(2*B.y);
                    tmp.x=x,tmp.y=tmp.x*k2+b2;
                    xin[count++]=tmp;
                }
                else   //B点斜率不存在 
                {
                    x=B.x/2;
                    k1=-(A.x/A.y);
                    b1=(pow(A.x,2)+pow(A.y,2))/(2*A.y);
                    tmp.x=x,tmp.y=tmp.x*k1+b1;
                    xin[count++]=tmp;
                }
            }
        }
    }
    sort(xin,xin+count,cmp);
     tmp=xin[0];
    for(int i=0;i<count;i++)
    {
        if(equals(tmp.x,xin[i].x)&&equals(tmp.y,xin[i].y))
        {
            tmpnum++;
        }
        else
        {
            ans=max(ans,tmpnum);
            tmpnum=1;
            tmp=xin[i];
        }
    }
    ans=max(ans,tmpnum); 
    tmpnum=ans;
    ans=0;
    for(int i=1;i<2005;i++) //根据圆心的数量求出点的数量 
    {
        if(i*(i-1)/2<=tmpnum)
        {
            ans=i;
        }
    }
    cout<<ans<<endl;
    return 0;
}

三、Cover the Tree(C)

题目链接: Cover the Tree
题目大意: 给你一颗n个节点的树,问你最少用多少条链能把每一条边都覆盖,并且输出链的两端的节点。
解题思路: 我们可以猜想这链的两端是叶子节点是比较好的,因为不是叶子节点的话可以把这个链延伸到叶子节点,但是链数不增加。那么怎样的叶子节点在一条链上呢,我们需要让一条链上的叶子节点尽可能的不相邻,这样一条链上才有更多的点。我们对这棵树跑一遍dfs,把每个叶子节点按照dfs序记录下来。如果叶子节点的个数是偶数,那么前一半的第一个对应后一半的第一个,以此类推。如果叶子节点是奇数,那么就用根节点去和其中一个叶子节点成链,然后接着用偶数的规律。具体证明看下图。(具体细节看代码)
在这里插入图片描述
代码:

#include<iostream>
#include<iomanip>
#include<cmath>
#include<map> 
#include<string>
#include<vector>
#include<set> 
#include<queue> 
#include<algorithm>
#include<string.h>


#define MAXN 200005
using namespace std;
typedef long long ll;
const int N=200005;
struct edge{
	int to;
};
int ye[N];
int du[N];
int flag[N];
vector<edge> V[N];

int ans=0;

void dfs(int x)
{
	flag[x]=1;
	int mark=0;
	for(int i=0;i<V[x].size();i++)
	{
		int y=V[x][i].to;
		if(flag[y]==0)
		{
			mark=1;
			dfs(y);
		}
	}
	if(mark==0)
	{
		ye[ans++]=x;
	}
	
}
int main()
{
	int n;
	int a,b;
	int s;
	scanf("%d",&n);
	for(int i=0;i<n-1;i++)
	{
		scanf("%d%d",&a,&b);
		V[a].push_back(edge{b});
		V[b].push_back(edge{a});
		du[a]++;
		du[b]++;
	}
	for(int i=1;i<=n;i++)
	{
		if(du[i]!=1)
		{
			s=i;
			break;
		}
	}
	dfs(s);
	
	printf("%d\n",ans/2+ans%2);
	if(ans&1)
	ye[ans++]=s;
	for(int i=0;i<ans/2;i++)
	{
		if(ye[i]>ye[i+ans/2])
		swap(ye[i],ye[i+ans/2]);
		printf("%d %d\n",ye[i],ye[i+ans/2]);
	}
	return 0;
}


四、Fake Maxpooling(F)

题目链接: Fake Maxpooling
题目大意: 给你一个n * m大小的矩阵,这个矩阵内的每一个数的大小为 A i , j = l c m ( i , j ) A_{i,j}=lcm(i,j) Ai,j=lcm(i,j),现在给你一个k * k的矩阵大小,问你在n * m矩阵中的每一个k * k的矩阵的最大数字的和是多少。
解题思路: 我们可以先想一想在一维情况下,我们求每一个固定大小范围的最大值的和该怎么求,很明显我们用一个单调队列,每次队首都代表对应范围的最大值。那么二维我们也可以用类似的方法,这里我们需要一个单调队列数组,每一个单调队列用来表示每一行的情况,然后在额外用一个单调队列,从单调队列数组中的每一个队首元素取值,每次的结果都是一个k * k大小的矩阵的元素的最大值。还要说的是对于n * m矩阵的赋值存在一种优化操作(具体细节看代码)
代码:

#include<iostream>
#include<iomanip>
#include<cmath>
#include<map> 
#include<string>
#include<vector>
#include<set> 
#include<queue> 
#include<algorithm>
#include<string.h>


#define MAXN 5050
using namespace std;
typedef long long ll;
int num[MAXN][MAXN];
int gcd[MAXN][MAXN];
int n,m;
ll lcm(int a, int b) {
    return a / __gcd(a, b) * b;
}
void init() //题解中的优化 
{
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(!gcd[i][j])
			{
				for(int k=1;k*i<=n&&k*j<=m;k++)
				{
					gcd[k*i][k*j]=k;
					num[k*i][k*j]=i*j*k;
				}
			}
		}
	}
}
struct node{
	int v,pos;
};
ll ans=0;
deque<node> dues[MAXN];
deque<node> due2;
int main()
{
	int k;
	scanf("%d%d%d",&n,&m,&k);
	init();
	for(int j=1;j<=m;j++)
	{
		for(int i=1;i<=n;i++) //处理每一行的信息 
		{
			node tmp={num[i][j],j};
			if(j==1)
			dues[i].push_back(tmp);
			else
			{
				if(j-dues[i].front().pos>=k)
				dues[i].pop_front();
				while(!dues[i].empty()&&dues[i].back().v<tmp.v)
				dues[i].pop_back();
				dues[i].push_back(tmp);
			}		
		}
		if(j>=k) //从第k列开始,求取结果 
		{
			due2.clear();
			for(int i=1;i<=n;i++) //对每一个单调队列的队首元素 在进行一次单调队列处理 
			{
				node tmp={dues[i].front().v,i};
				if(i==1)
				due2.push_back(tmp);
				else
				{
					if(i-due2.front().pos>=k)
					due2.pop_front();
					while(!due2.empty()&&due2.back().v<tmp.v)
					due2.pop_back();
					due2.push_back(tmp);
				}
				
				if(i>=k)
				{
					ans+=due2.front().v; //每次的结果都是一个k*k大小矩阵的最大元素的值 
//					cout<<due2.front().v<<endl;
				}
				
			}
		}
//		cout<<"*"<<endl;
	}
	cout<<ans<<endl;
	return 0;
}


五、Greater and Greater(G)

题目链接: Greater and Greater
题目大意: 现在有一个长度为n的序列A,和一个长度为m的序列B,现在问你在A中有多少个长度为m的子序列,可以满足每一位数字都对应的大于B中的每一位数字。
解题思路: 这个题暴力的方法就是通过移动B,每次都进行比较,时间复杂度为n * m,显然是不好的。我们将A的每一位都与B的每一位进行比较,若A中的数字大于B中的数字,那么记为1,否则记为0。
例如:
A:1 4 2 8 5 7
B:2 3 3
那么比较结果就是:
0 1 1 1 1 1
0 1 0 1 1 1
0 1 0 1 1 1
我们可以发现,如果斜着的三个比较结果都是1的话那么就存在一组解,我们通过平移把它对齐(第二行向左平移1个单位,不足的补0),对齐结果如下:
0 1 1 1 1 1
1 0 1 1 1 0
0 1 1 1 0 0
我们现在可以看出在第3和第4列整列都是1,因此对应的结果是(2,8,5),(8,5,7)。也就是说我们可以通过这样的方法来求取结果,但是我们计算出这些个比较结果的时间复杂度仍然是n * m。我们其实可以发现B中的相同的数字的比较结果是相同的,只是平移的单位是不同的。那么我们其实是可以进行优化的,例如,如果A中的某些数字大于B中的数字3,那么A中的这些数字一定大于B中的数字2,那么这些数字和2就不用了比较了。具体我们对A,和B从大到小进行排序,然后先对B中大的数字进行比较,比较结果可以继承给B中小的数字。这里我们用到bitset,bitset里面的元素只有1或者0,并且可以进行移位操作,与或操作,非常合适。
代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
 
const int N=150005;
struct str{
	int v,pos; //需要记录每个数字的位置,B中数字的位置,决定平移多少单位 
}A[N],B[40005]; 

bool cmp(str a,str b)
{
	return a.v>b.v;  //从大到小排序 
}

bitset<N> tmp,ans;
int main(){
  int n,m;
  ans.set(); //结果全部初始化为1 
  tmp.reset(); //比较结果初始化为0 
  scanf("%d%d",&n,&m);
  for(int i=0;i<n;i++)
  {
  	scanf("%d",&A[i].v);
  	A[i].pos=i;
  }
  for(int i=0;i<m;i++)
  {
  	scanf("%d",&B[i].v);
  	B[i].pos=i;
  }
  sort(A,A+n,cmp);	//从大到小排序 
  sort(B,B+m,cmp);   //从大到小排序 
  for(int i=0,j=0;i<m;i++)   //遍历B中的每一个元素 
  {
  	while(j<n&&A[j].v>=B[i].v)  
  	{
  		tmp.set(A[j++].pos); //我们是在大的数字的比较结果上,进行继承。 
	  }
	  ans&=(tmp>>(B[i].pos)); //因为在bitset中低位是在右边的因此是往右移
	  							//bitset实质是转化为十进制数字 
  }
  cout<<ans.count()<<endl;
}


参考博客

https://www.cnblogs.com/whitelily/p/13311327.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值