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);
}

浙公网安备 33010602011771号