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

第二场

第二场感觉适合补的题比较多。

A.All-with-Pairs

可以通过map记录所有后缀出现的次数,并对每个前缀都计算它出现在后缀的次数,复杂度O(n)。但需要考虑到重复计算的情况(比如每当aba前后缀匹配时,会有一个a被重复计算)。

解决方法是预处理出类似于kmp算法中的nxt数组,从前往后扫一遍cnt[nxt[j]]-=cnt[j]。细节要扣清楚还是挺麻烦的。

使用map时不能直接将string作为键值(会爆内存),需要对字符串取不同的参数做两次哈希,把一对哈希值作为键值。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,ll> pii;
const int MAXN=1e6+5,MOD=998244353,MOD1=998244353,MOD2=1e9+7;

const int b1=131,b2=29;

ll base1,base2;

int n;

string s[MAXN];

map<pii,int>CNT;

int cnt[MAXN],nxt[MAXN];

ll ans;

void getNext(string &s,int *nxt)//求失配nxt
{
    nxt[0] = -1;
    for (int i=1,j=-1;i<=s.length();i++)
    {
        while(j!=-1 && s[i-1]!=s[j]) j=nxt[j];
        nxt[i]=++j;
    }
}
ll hashv1,hashv2;

int main()
{
    cin>>n;
    for (int i=1;i<=n;i++)
    {
        cin>>s[i];
        base1=1,base2=1,hashv1=hashv2=0;
        for (int j=s[i].length()-1;j>=0;j--)
        {
            hashv1=((1ll*(s[i][j])*base1)%MOD1+hashv1)%MOD1;
            base1=(1ll*base1*b1)%MOD1;
            hashv2=((1ll*(s[i][j])*base2)%MOD2+hashv2)%MOD2;
            base2=(1ll*base2*b2)%MOD2;
            CNT[{hashv1,hashv2}]++;
        }
    }
    for (int i=1;i<=n;i++)
    {
        hashv1=hashv2=0;
        for (int j=0;j<s[i].length();j++)
        {
            hashv1=((1ll*hashv1*b1)+(s[i][j]))%MOD1;
            hashv2=((1ll*hashv2*b2)+(s[i][j]))%MOD2;
            cnt[j+1]=CNT[{hashv1,hashv2}];
        }
        getNext(s[i],nxt);
        for (int j=1;j<=s[i].length();j++)
        {
            cnt[nxt[j]]-=cnt[j];
        }
        for (int j=1;j<=s[i].length();j++)
        {
            ans=(ans+((1ll*cnt[j]*j)%MOD*j)%MOD)%MOD;
            cnt[j]=0;
        }
    }
    printf("%lld\n",ans);
    return 0;
}

G.Greater-and-Greater

本质上是dp。用dp[i][j]表示 ∀ k ∈ [ 0 , m ] \forall k \in[0,m] k[0,m]满足 a i + k > b j + k a_{i+k}>b_{j+k} ai+k>bj+k (考虑 [ a i , a i + 1 , . . . , a i + m − j ] 和 [ b j , b j + 1 , . . . , b m ] [a_i,a_{i+1},...,a_{i+m-j}]和[b_j,b_{j+1},...,b_{m}] [ai,ai+1,...,ai+mj][bj,bj+1,...,bm])

画张图表示一下,dp[i][j]=1就表示第一段的每一位都大于等于第二段:
在这里插入图片描述
接下来考虑转移,在已知dp[i][j]=1的情况下,要确定dp[i-1][j-1](即图中的三、四两段),只需要再比较a[i-1]和b[i-1]的大小即可
在这里插入图片描述
写成代码就是

if (dp[i][j] && a[i-1]>=b[j-1]) dp[i-1][j-1]=1;

最后只要统计dp[i][1]的数量就是答案了。

但如果用常规方法来实现这个dp,无论空间还是时间上都是不可做的,然而bitset可以。(bit比bool节约八倍空间,位运算节约时间)思想其实就是上面dp的思路,只不过要实现出来真的只能说是神仙操作。

#include<bits/stdc++.h>
#define debug(x) cerr<<#x<<" : "<<x<<endl;
using namespace std;
const int N=1.5e5+5,M=4e4+5;

bitset<M>cur,S[M],ns,I;

int T,n,m,ans;

int A[N],B[M],ord[M];

int fd(int x)
{
    int l=0,r=m;
    while(l<r)
    {
        int mid=(l+r)>>1;
        if (B[ord[mid+1]]<=x)
            l=mid+1;
        else r=mid;
    }
    return l;
}
int main()
{
    cin>>n>>m;
    for (int i=1;i<=n;i++) scanf("%d",&A[i]);
    for (int i=1;i<=m;i++)
    {
        scanf("%d",&B[i]);
        ord[i]=i;
    }
    sort(ord+1,ord+1+m,[&](int u,int v){
        return B[u]<B[v];
    });
    for (int i=1;i<=m;i++)
    {
        S[i]=S[i-1];
        S[i].set(ord[i]);
    }
    I.set(m);
    for (int i=n;i>=1;i--)
    {
        ns=S[fd(A[i])];
        cur=((cur>>1)|I)&ns;
        if (cur[1]) ans++;
    }
    printf("%d\n",ans);
    return 0;
}

第三场

C.Operation-Love

按照顺序依次计算两点之间的距离

如果(a,b)长度为6,(b,c)长度为1,说明(a,b)是拇指的线段(指向指尖)

如果(a,b)长度为6,(b,c)长度为9,说明(b,a)是拇指的线段(指向指尖)

如果是左手,那么其他所有点都在拇指线段的左侧,反之则在右侧。

判断点在线段左侧还是右侧可以通过叉乘的方法。

假设线段为(x1,y1)->(x2,y2),点坐标为(x0,y0)

那么计算(x2-x1,y2-y1)×(x0-x1,y0-y1)的值,<0为右侧,>0为左侧

#include<bits/stdc++.h>
#define debug(x) cerr<<#x<<" : "<<x<<endl;
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int MAXN=2e3+5;
const double eps=1e-4;

class Point {
public:
    double x,y;
    bool operator !=(Point &P)
    {
        return fabs(x-P.x)>eps && fabs(y-P.y)>eps;
    }
    Point(double a=0,double b=0):x(a),y(b){}
};
class Line {
public:
    bool ifk;
    double dis;
    double k,b;
    //求过两点的直线
    void mkline (Point A,Point B) {
        dis=sqrt((A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y));
        ifk=true;k=b=0;
        if (A.x==B.x) {
            ifk=0;
            b=A.x;
        } else {
            if (A.y==B.y) {
                k=0;
                b=(A.x+B.x)/2;
            } else {
                k=(B.y-A.y)/(B.x-A.x);
                b=A.y-k*A.x;
            }
        }
    }
    //求两点中垂线
    void mkbisector (Point A,Point B) {
        ifk=true;k=b=0;
        if (A.x==B.x) {
            k=0;
            b=(A.y+B.y)/2;
        } else if (A.y==B.y) {
                ifk=0;
                b=(A.x+B.x)/2;
            } else {
                k=-1/(B.y-A.y)*(B.x-A.x);
                b=(A.y+B.y)/2-k*(A.x+B.x)/2;
            }
    }
    bool operator == (Line &T) {
        return (ifk==T.ifk) && fabs(k-T.k)<eps && fabs(b-T.b)<eps;
    }
};

Point p[30];

Line l[30];

int T;
int main()
{
    cin>>T;
    while(T--)
    {
        for (int i=1;i<=20;i++)
        {
            cin>>p[i].x>>p[i].y;
        }
        p[21]=p[1];
        for (int i=1;i<=20;i++)
        {
            l[i].mkline(p[i],p[i+1]);
        }
        l[21]=l[1];
        Point a,b,c;
        for (int i=1;i<=20;i++)
        {
            if (fabs(l[i].dis-6)<eps && fabs(l[i+1].dis-1)<eps)
            {
                a=p[i],b=p[i+1];
            }
            if (fabs(l[i].dis-6)<eps && fabs(l[i+1].dis-9)<eps)
            {
                a=p[i+1],b=p[i];
            }
        }
        for (int i=1;i<=20;i++)
            if (p[i]!=a && p[i]!=b)
            {
                c=p[i];
                break;
            }
        double p=b.x-a.x,q=b.y-a.y,w=c.x-a.x,o=c.y-a.y;
        if (p*o-q*w<0) printf("right\n");
        else printf("left\n");
    }
    return 0;
}

E.Two-Matchings

本身是一个图论问题,要使得每个点都连上两条边且边的权值和最小,权值为两个数之差的绝对值。可以把n个数从小到大排序,把问题变成在一列有序数上连线的问题。

首先考虑到n=4或n=6的情况,可以发现此时答案是固定的,也就是(A[n]-A[1])*2,而对于n>=8的情况,总是可以拆成若干个长度为4或6的分块,并使答案减少分块连接处的两数之差。也就是dp[n]总是从dp[n-4]或dp[n-6]减去连接处的权值后转移过来,这样就可以通过dp计算答案了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=2e5+5;

const ll INF=0x3f3f3f3f3f3f3f3f;

int T,n,m,A[N];

ll dp[N];

int main()
{
    cin>>T;
    while(T--)
    {
        cin>>n;
        for (int i=1;i<=n;i++)
        {
            scanf("%d",&A[i]);
            dp[i]=INF;
        }
        sort(A+1,A+1+n);
        dp[0]=0;
        for (int i=4;i<=n;i+=2)
        {
            if (i>=4) dp[i]=min(dp[i],dp[i-4]+(A[i]-A[i-3])*2);
            if (i>=6) dp[i]=min(dp[i],dp[i-6]+(A[i]-A[i-5])*2);
        }
        printf("%lld\n",dp[n]);
    }
    return 0;
}

F.Fraction-Construction-Problem

如果a,b不互质那么答案比较显然,取 a b \frac{a}{b} ba约分之后的分母即可。

考虑a、b互质的情况,把原式转换成 c f − e d d f = a b \frac{cf-ed}{df}=\frac{a}{b} dfcfed=ba

如果b自身是质数或者1,那么d、f中至少有一个大于等于b,不符合条件。

如果b不是质数且不是1,那么d总能分解成两个互质因子,令b的两个互质因子分别为d、f,那么由扩展欧几里得算法可知 c f − e d = a cf-ed=a cfed=a一定有整数解。将d、f代入后求正整数解即可。

为了求d的互质因子,在素数筛中可以加入pfactor[i]数组记录i的第一个质因数。将第一个质因数全部取出后,分离出的两个数一定互质。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
 
const int MAXN=2e6+5;
 
int T,n,m,a,b;
 
int vis[MAXN],prime[MAXN],num_p;
int pfactor[MAXN];
void getPrime()
{
    for (int i=2;i<MAXN;i++)
    {
        if (!vis[i]) prime[++num_p]=i;
        for (int j=1;j<=num_p;j++)
        {
            if (i*prime[j]>=MAXN) break;
            pfactor[i*prime[j]]=prime[j];
            vis[i*prime[j]]=1;
            if (i%prime[j]==0) break;
        }
    }
}
 
void Exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) x = 1, y = 3;
    else Exgcd(b, a % b, y, x), y -= a / b * x;
}
 
void solve()
{
    cin>>a>>b;
    ll c,d,e,f;
    int g=__gcd(a,b);
    if (g!=1)
    {
        d=f=b/g;
        e=1,c=a/g+1;
        printf("%lld %lld %lld %lld\n",c,d,e,f);
        return;
    }
    a/=g,b/=g;
    if (pfactor[b]!=0)
    {
        int d=1,f=b;
        while(f%pfactor[b]==0) f/=pfactor[b],d*=pfactor[b];
        if (f!=1) {
            Exgcd(f,d,c,e);
            e=-e;
            while(c<=0 || e<=0)
                c+=d,e+=f;
            c*=a,e*=a;
            printf("%lld %lld %lld %lld\n",c,d,e,f);
            return;
        }
    }
    printf("-1 -1 -1 -1\n");
}
int main()
{
    cin>>T;
    getPrime();
    while(T--) solve();
    return 0;
}

G.Operating-on-a-Graph

其实就是并查集,但是需要一些技巧。

两个重点,一是启发式合并,将b合并入a时,若b中元素较多,则直接交换a、b中的元素。

二是合并前清空原有的元素,合并后只保留新增的元素。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;

const int MAXN=1e6+5;

int T,n,m,q,t,u,v;

int FA[MAXN];
vector<int>G[MAXN];
vector<int>group[MAXN];

int fnd(int x)
{
    if (FA[x]==x) return x;
    else return FA[x]=fnd(FA[x]);
}
void solve()
{
    cin>>n>>m;
    for (int i=0;i<n;i++)
    {
        G[i].clear();
        group[i].clear();
        FA[i]=i;
        group[i].push_back(i);
    }
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    cin>>q;
    while(q--)
    {
        scanf("%d",&t);
        if (fnd(t)!=t) continue;
        vector<int>now;
        for (auto h:group[t])
            for (auto to:G[h])
            {
                now.push_back(fnd(to));
            }
        group[t].clear();
        for (auto x:now)
        {
            if (fnd(x)==t) continue;
            FA[x]=t;
            if (group[x].size()>group[t].size()) swap(group[x],group[t]);
            for (auto tt:group[x]) group[t].push_back(tt);
            group[x].clear();
        }
    }
    for (int i=0;i<n;i++) printf("%d ",fnd(i));
    printf("\n");
}
int main()
{
    cin>>T;
    while(T--) solve();
    return 0;
}

第四场

H.Harder-Gcd-Problem

构造。除了1和大于n/2的质数,若剩下的数的个数为偶数则都能匹配,若为奇数则只有一个不能匹配。

从大到小考虑所有小于等于n/2的素数p,将未被访问的p的倍数加入集合,若集合大小为偶数,则恰好两两匹配,若集合大小为奇数,则取出p*2这个数(因为2是最后考虑的素数,所以p*2一定没有被访问过),剩下的两两匹配。最后把所有取出来的数再两两匹配一下(都是2的倍数)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;

const int MAXN=2e5+5,mod=1e9+7;

int T,a,b,c,d,n,cnt;
int vis[MAXN],prime[MAXN],num_p;
int pfactor[MAXN];
void getPrime()
{
    for (int i=2;i<MAXN;i++)
    {
        if (!vis[i]) prime[++num_p]=i;
        for (int j=1;j<=num_p;j++)
        {
            if (i*prime[j]>=MAXN) break;
            pfactor[i*prime[j]]=prime[j];
            vis[i*prime[j]]=1;
            if (i%prime[j]==0) break;
        }
    }
}
bool visit[MAXN];

vector<pii>ans;
int main()
{
    getPrime();
    cin>>T;
    while(T--)
    {
        cin>>n;
        for (int i=1;i<=n;i++) visit[i]=0;
        int p=1;
        while(prime[p]<=n/2) p++;
        p--;
        vector<int>now;
        vector<int>tot;
        ans.clear();
        for (int i=p;i>=1;i--)
        {
            now.clear();
            for (int j=prime[i];j<=n;j+=prime[i])
            {
                if (!visit[j])
                {
                    visit[j]=1;
                    now.push_back(j);
                }
            }
            while(now.size()>3)
            {
                int x=now.back();
                now.pop_back();
                int y=now.back();
                now.pop_back();
                ans.push_back({x,y});
            }
            if (now.size()==3)
            {
                int x=now.back();
                now.pop_back();
                tot.push_back(now.back());
                now.pop_back();
                int y=now.back();
                now.pop_back();
                ans.push_back({x,y});
            }
            else
            if (now.size()==2)
            {
                int x=now.back();
                now.pop_back();
                int y=now.back();
                now.pop_back();
                ans.push_back({x,y});
            }
        }
        while(tot.size()>=2)
        {
            int x=tot.back();
            tot.pop_back();
            int y=tot.back();
            tot.pop_back();
            ans.push_back({x,y});
        }
        printf("%d\n",ans.size());
        for (auto xx:ans) printf("%d %d\n",xx.first,xx.second);
    }
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值