9.4 上午 becoder 模拟赛总结 & 题解

T1 东方记者

简单 DP,设 \(dp_{i,j}\) 表示最后走到了第 \(i\) 个事件的发生地点,且一共收集了 \(j\) 处资料的最小移动距离。

根据定义,可以知道对于所有 \(dp_{i,j}\),如果走回原点后,依然满足移动距离小于 \(d\),则有:\(ans=max\{j\}\)

状态转移也很简单:\(dp_{i,j}=min_{k=0}^{k<i}\{dp_{k,j-1}+\lvert x_i-x_k\rvert+\lvert y_i-y_k\rvert\}\),初值 \(dp_{0,0}=x_0=y_0=0\)

不作过多赘述,代码如下(100pts):

#define N 105
long long n,d,ans,x[N],y[N],dp[N][N];
int main(){
    memset(dp,0x3f,sizeof dp),dp[0][0]=0,scanf("%lld",&n);
    for(int i=1;i<=n;i++)  dp[i][0]=0,scanf("%lld%lld",x+i,y+i);
    scanf("%lld",&d);
    for(int i=1;i<=n;i++){
        for(int j=0;j<i;j++)
            for(int k=1;k<=j+1;k++)
                dp[i][k]=min(dp[i][k],dp[j][k-1]+abs(x[i]-x[j])+abs(y[i]-y[j]));
        for(int j=ans;j<=i;j++)  if(dp[i][j]+abs(x[i])+abs(y[i])<=d)  ans=j;
    }
    printf("%lld\n",ans);
}

T2 密钥破解

先说 30pts 的做法:

先通过试除法给 \(N\) 分解质因数求出 \(p\)\(q\),然后就能求出 \(r\)

\(d\) 就是 \(e\) 在模 \(r\) 意义下的逆元,用 exgcd 求出就行了,最后 \(n\) 快速幂计算 \(c^d \mod N\) 的值就可以了。

100pts 的做法只是在此基础上把分解质因数的方法换成了 Pollard Rho,并且因为 \(N\) 比较大,需要龟速乘或者 __int128 进行计算。

代码如下,思路其实很简单,主要难度在 Pollard Rho(100pts):

#define LL long long
LL e,n,c,r;
LL quick_mul(LL x,LL y,LL mod)//x和y的积对mod取模
LL quick_pow(LL x,LL y)//x的y次方对n取模
void exgcd(LL a,LL b,LL &x,LL &y){
    if(!b)  return x=1,y=0,void();
    exgcd(b,a%b,y,x),y=(y-quick_mul(a/b,x,r)+r)%r;
}
mt19937 mrand(time(NULL));
LL gcd(LL x,LL y){return !y?x:gcd(y,x%y);}
LL pollard_rho(LL c){
    LL i=0,k=2,x=mrand()*mrand()%(n-1)+1,y=x,d;
    while(1){
        x=(quick_mul(x,x,n)+c)%n,d=gcd((x-y+n)%n,n);
        if(d!=1&&d!=n)  return d;
        if(x==y)  return n;
        if(++i==k)  k<<=1,y=x;
    }
}
int main(){
    scanf("%lld%lld%lld",&e,&n,&c);
    LL p=n,d,tmp;
    while(p==n)  p=pollard_rho(mrand()*1ll*mrand()%(n-1)+1);
    r=(p-1)*(n/p-1),exgcd(e,r,d,tmp);
    return printf("%lld %lld\n",d,quick_pow(c,d)),0;
}

T3 琪露诺数

经典数位 DP,但是是高精度版本。

对于数位 DP 枚举到的这一位,如果是回文串的前半段,就正常枚举就行。

如果是后半段就不用枚举了,直接判断对应位的取值在这一位能否取到,能就继续递归,不能就直接返回 0,递归到末尾时返回 1。

有个小技巧就是在后半段时如果 \(limit\) 已经为 0,也就是可以任意取值的时候,可以直接返回 1。

至于如何判断是在前后哪一个半段,记搜的时候加一个值 \(st\) 表示是从哪一位开始不是前导 0 的,也就是记录数的位数。

本题的主要难点不在于数位 DP,而是高精度的加减乘除,代码放下面,高精度部分不做赘述(100pts):

#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL t,num[205],s[205];
string l,r,dp[205][205];
string add(string a,string b)//高精度十进制整数a+b
string sub(string a,string b)//高精度十进制整数a-b
string mul(string a,string b)//高精度十进制整数a*b
string div(string a,string b,string res="")//高精度十进制整数a/b(向下取整)
string dfs(int pos,int st,bool limit){
    if(!pos)  return "1";
    if(st!=-1&&!limit&&dp[pos][st]!="")  return dp[pos][st];
    if(st-pos>=pos){
        if(!limit)  return "1";
        if(num[pos]<s[st-pos+1])  return "0";
        return dfs(pos-1,st,s[st-pos+1]==num[pos]);
    }
    string res="0";
    for(int i=0;i<=(limit?num[pos]:8);i++){
        s[pos]=i;
        if(!i&&st==-1)  res=add(res,dfs(pos-1,st,limit&&i==num[pos]));
        else if(st==-1)  res=add(res,dfs(pos-1,pos,limit&&i==num[pos]));
        else  res=add(res,dfs(pos-1,st,limit&&i==num[pos]));
    }
    if(st!=-1&&!limit) dp[pos][st]=res;
    return res;
}
string solve(string x,int pos=0){
    if(x=="0")  return "1";
    while(x!="0"){
        string tmp=div(x,"9");
        num[++pos]=sub(x,mul(tmp,"9"))[0]-'0',x=tmp;
    }
    return dfs(pos,-1,1);
}
int main(){
    cin>>t;
    while(t--){
        cin>>l>>r;
        cout<<sub(solve(r),solve(sub(l,"1")))<<"\n";
    }
}

T4 御神渡

先来看到大家都能想到的 40pts 做法:建完全图,然后 \(O(n^2 \log^2 n)\) 的最小生成树,我们接下来以此为基础进行优化。

首先,我们可以知道对于任意一个点,与它相连的最短的边一定在最小生成树中,但不一定只有这一条边在。

所以我们可以先来求出每个点与它相连最短的边是哪一条,然后把这一条边先给连上。

这种求最小生成树的算法叫做 Boruvka,本质是 Kruskal 与 Prim 算法的结合。

接下来我们来考虑如何求最短的边,根据题意可以得出满足 \(C_i-C_j<A_x(A_i-A_j)\) 时选 \(i\) 比选 \(j\) 要更优。

很明显这是一个斜率优化的式子,设第 \(x\) 个点为 \((A_x,C_x)\),这个式子就等价于 \(K_{i,j}<A_x\)\(K\) 为直线斜率。

所以我们就可以按 \(A\) 排序,用单调队列来帮助我们求出对于每个点边权最小的边了。

但这样做有一个问题,就是有可能会产生自环,所以对于每一个点,我们都必须把它排除在外来求边。

那我们就对于每个点 \(O(n)\) 求一次咯?那复杂度不久变回去了吗?这肯定是不行的。

但可以发现如果把一个区间拆成 \([l,mid]\)\((mid,r]\),那么两段区间就可以互相选取了,于是我们就可以通过分治完成了。

这样就做完了吗?肯定是没有的,因为我们现在只考虑了连最短的边,但是可能有些边的左右端点都选的这一条,图就没有连通了。

那接下来我们怎么做呢?这里可以考虑把每一个连通块内部都染成同种颜色,并让外部两两不同。

这样我们边的选取条件就变为了:每次每个连通块从颜色不同的连通块中选取一条最短的边并连上。

这样的操作最多只会进行 \(\log n\) 次,因为就算每次都只有两个连通块间互相连的边,连通块都会减少一半。

虽然复杂度保证了,但现在既要保证 \(A\) 的有序,又要保证颜色不同,这个过程又怎么去维护呢?

可以发现这两个条件实际上就是二维偏序,那我们就可以祭出祖传算法 cdq 了。

这样总共做 \(\log n\) 次 cdq,而每次的时间复杂度是 \(O(n \log n)\) 的,总的时间复杂度就是 \(O(n \log^2 n)\) 了。

做法大概就是这样,最后也是最难闯过的难点就在代码实现上了,所以我就贴心的把代码贴在下面吧(100pts,注释最多的一次):

#include<bits/stdc++.h>
using namespace std;
#define N 500005
#define LL long long
LL ans,w[N];//w表示这个连通块这次连的边的权值
int n,d[N],rk[N],to[N],tmp[N];
//d是并查集数组,rk用于将所有的颜色做离散化处理,to表示这个连通块这次连的谁,tmp其实就是单调队列
struct Node{LL col,x,y;}s[N],smp[N];//把(A[i],C[i])视作一个点,颜色为col,smp则用于做归并排序
bool cmp(Node a,Node b){//优先颜色不同,其次按A从小到大排序
    if(a.col!=b.col)  return a.col<b.col;
    return a.x<b.x;
}
int find(int x){return x==d[x]?x:d[x]=find(d[x]);}//并查集找祖先
LL calc(int i,int j){return s[i].y+s[j].y-s[i].x*s[j].x;}//求i和j的连边的边权
int* work(int l,int r,int *head){//单调队列维护下凸包
    int *tail=head;
    for(int i=l;i<=r;i++){
        while(tail-head>1&&(s[i].x-s[*(tail-1)].x)*(s[*(tail-1)].y-s[*(tail-2)].y)>=(s[*(tail-1)].x-s[*(tail-2)].x)*(s[i].y-s[*(tail-1)].y))  tail--;
        *(tail++)=i;//上面一行是斜率比较的乘法形式
    }
    return tail;
}
void cdq(int cl,int cr,int l,int r){//cdq分治模板...吧?,[cl,cr]是离散化后的颜色区间,[l,r]是s数组的区间
    if(cl==cr)  return;
    int cmid=(cl+cr)>>1,mid=l,fx,fy,cnt=l;
    while(rk[s[mid+1].col]<=cmid)  mid++;//找到两块颜色区间的分界点
    cdq(cl,cmid,l,mid),cdq(cmid+1,cr,mid+1,r);
    for(int i=mid+1,*now=tmp,*tail=work(l,mid,tmp);i<=r;i++){//单调队列,右边连左边
        fx=1,fy=s[i].x;
        while(tail-now>=2&&(s[*(now+1)].x-s[*now].x)*fy>=fx*(s[*(now+1)].y-s[*now].y))  now++;
        if(to[s[i].col]==-1||calc(i,*now)<w[s[i].col])  w[s[i].col]=calc(i,*now),to[s[i].col]=s[*now].col;
    }
    for(int i=l,*now=tmp,*tail=work(mid+1,r,tmp);i<=mid;i++){//单调队列,左边连右边
        fx=1,fy=s[i].x;
        while(tail-now>=2&&(s[*(now+1)].x-s[*now].x)*fy>=fx*(s[*(now+1)].y-s[*now].y))  now++;
        if(to[s[i].col]==-1||calc(i,*now)<w[s[i].col])  w[s[i].col]=calc(i,*now),to[s[i].col]=s[*now].col;
    }
    for(int i=l,j=mid+1;i<=mid||j<=r;){//按A从小到大归并排序
        if(j>r||i<=mid&&s[i].x<s[j].x)  smp[cnt++]=s[i++];
        else  smp[cnt++]=s[j++];
    }
    for(int i=l;i<=r;i++)  s[i]=smp[i];
}
bool solve(int tot=0){//每一次操作返回1代表只有一个连通块了,返回0代表不止一个连通块,应该继续操作
    for(int i=1;i<=n;i++)  d[i]=i,to[i]=-1;
    sort(s+1,s+n+1,cmp);
    for(int i=1;i<=n;){//对颜色离散化处理,rk[i]表示第i个颜色离散化后的值
        rk[s[i].col]=++tot;
        while(i<=n&&rk[s[i].col]==tot)  i++;
    }
    if(tot==1)  return 1;//只有一个连通块,不用操作了
    cdq(1,tot,1,n);
    for(int i=1;i<=n;i++){//统计本次操作的答案
        if(!rk[s[i].col])  continue;//一个连通块只统计一次
        rk[s[i].col]=0;
        if(to[to[s[i].col]]==s[i].col&&s[i].col>to[s[i].col])  continue;//两端点连到同一条边了
        d[s[i].col]=to[s[i].col],ans+=w[s[i].col];
    }
    for(int i=1;i<=n;i++)  s[i].col=find(s[i].col);//求每个点现在所在的连通块
    return 0;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)  scanf("%lld",&s[i].x);
    for(int i=1;i<=n;i++)  scanf("%lld",&s[i].y),s[i].col=i;
    while(!solve());printf("%lld\n",ans);
}
posted @ 2024-09-04 15:02  tkdqmx  阅读(97)  评论(0)    收藏  举报