I题:

简化题意:给出举例d和一条长度为l的线段,每走一段长度为1的距离都需要花费一定的体力,而你拥有3次直接跳过长度为d的线段的机会。问最少需要多少体力才能到线段终点

我在赛场上把这题建模为找三条长度为d的线段使他们的和最大(),然后就一直贪心贪寄了()这警告我们不要乱用贪心

实际上如果对dp熟悉的话这题基本一眼dp。突然感觉自己的水平拉完了因为后面的状态完全可以由前面转移过来。因为存在俩种走路方式:跳跃和走一段1,且存在边界限制(<=l)。所以直接正向dp是可以解决的.定义dp数组一维为剩余的跳跃次数i,一维为当前位置j。

如果是走一段1:dp[i][j+1]=min( dp[i][j+1] , dp[i][j]+cost[j+1] ).

如果跳跃:dp[i+1][min(l,j+d)]=min(dp[i+1][min(l,j+d)] , dp[i][j]);代码如下:

查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=2e5+5;
const int INF=1e18;
void solve(){
    int n,d;cin>>n>>d;
    int t0,t1,t2;
    cin>>t0>>t1>>t2;
    string s;cin>>s;
    vector<int> a(n+1);
    vector<vector<int>> dp(4,vector<int>(n+1,INF));
    for(int i=0;i<n;i++){
        if(s[i]=='0')a[i+1]=t0;
        else if(s[i]=='1')a[i+1]=t1;
        else a[i+1]=t2;
    }
    for(int i=0;i<4;i++)dp[i][0]=0;
    for(int i=0;i<4;i++){
        for(int j=0;j<=n;j++){
            if(j<n)dp[i][j+1]=min(dp[i][j+1],dp[i][j]+a[j+1]);
            if(i<3){
                int mx=min(n,j+d);
                dp[i+1][mx]=min(dp[i+1][mx],dp[i][j]);
            }
        }
    }
    cout<<dp[3][n];
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)solve();
}
/*
10 2  
10 20 5
0000000000
12 3
10 100 1
111000222000
*/

H题:(默认你已经了解了Nim博弈,如果不了解我之前有专门的Nim博弈专题讲这题,链接

简化题意:给出n个数和一个限制k,A和B只能在限制内选择一个数(<=k)将n个数其中一个减去该数,直到有人不得不选0时,此人失败。

尝试发现k=1的情况是平凡的,如果当前总和为奇数必胜,偶数必败。

当k>=2时如果你拿到了总和为奇数的情况时就可以让k=1.此时回到了必胜态。因此双方的最优策略一定是给对方总和为偶数的状态,此时我们每次只会选偶数去减,直到不得不选1.但是偶数的选择是多种多样的,我们很难在原数组的情况下去操作和计算,此时我们需要再次考虑转化。因为每次都只能选偶数,我们可以把它转化为选择一个数 x满足1<=x<=k/2.同时,所有的数也都可以对2向下取整。这样就转化为一个子问题:

在所有ai/2的数组中选择x使得该数组全部为0.

如果向下整除后a数组的和仍为偶数且k仍不为1时就可以一直转化直到k=1或者a数组的和变为奇数。因为k=1的情况是平凡的,因此我们对a数组的和变为奇数的情况做思考。如果a数组的和变为奇数的话,此时新k就可以取1这个子问题就出现了必胜态(Nim博弈结论)。进一步转化后,异或和的最低位1的位置就是最小的t使得子问题出现必胜态。

因此只要比较异或和的最低位1和k的大小即可确定胜负。如果k是要大于异或和最低位1所表示的数的(如:异或和10100就是100(4),如果k>=4,则必胜,否则必败)。总结来看代码相当简单,25行不到,但是相当难想。

查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve(){
    int n,k;cin>>n>>k;
    int x=0;
    for(int i=1;i<=n;i++){
        int y;cin>>y;
        x^=y;
    }
    if(x!=0 && (x&-x)<=k)cout<<"Alice";
    else cout<<"Bob";
    cout<<'\n';
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _=1;
    cin>>_;
    while(_--)solve();
}

C题:(默认你已经会了矩阵快速幂,如果不会我之前有矩阵快速幂专题专门讲这题,链接

首先,回顾题意:在一个有向图中有多个节点,每个节点初始状态是独立的,在第0天修建了m条有向边,之后k天,每天修建了一条有向边,给出q个询问和一个限制条件w,每个询问给出起点和终点,对于每个询问,求在恰好w步从起点到终点的所有路径中修建的有向边的边权最大值的最小值

首先我们将建边的时间作为边权,其次这个询问看起来很绕,我们可以拿样例举例:

                                                                                  image

比如询问6 5时因为到不了输出-1;询问2 3时,若w=1,则恰好一步可达,若w为3则2到3之后要先去1再回3,又1 3之间存在双向边且边权都为0,因此最大值仍为0.询问2 6时,如果w为1,则必不可达,因为没有直接过去的路径,如果w为3,则需要在5的位置自环走一圈再到6,此时最大值为5到6的边权4.

理解了题目之后,首先我们如果没见过类似的题目,肯定会把这题当纯图论,直接就顺手用邻接表或者链式前向星存图了。但是这题有一个点就是n的范围是要≤200的,这是一个显示提示,我们以后见到这类的显示提示可以从dp,dfs或者枚举等方面入手。本题的话是从邻接矩阵建图为切入点的,当我们用邻接矩阵记录下样例的第一个状态(初始化为正无穷):

                                                                       image

这是只走一步时的状态矩阵,当我们走第二步时,我们可以列出:

                                                                 image

观察俩步的状态矩阵,会发现原本从i到j只需要1步,而现在需要在中间多经过一个点k,即从i到k,再从k到j。因此第二个矩阵中对应的值就为min( A2(i,j), max( A1(i,k) , A1(k,j) ) ).进一步推广到更高次幂的状态,会发现:

A3可以由A1和A2推出,值可以为min( A3(i,j) ,max( A1(i,k) , A2(k,j) ))或min( A3(i,j) ,max( A2(i,k) , A1(k,j) ));A4的矩阵可以由A3和A1推出,也可以由俩个A2推出,出于后续处理的便利,我们规定A^k由A^(k-1)与A推出.(实际上换一种推出方式最后的结果是不会变的)

以此类推,会发现,走w步实际上就是矩阵为w次幂时的状态,因此我们只需要求取w状态的矩阵即可在O(1)的复杂度下得出答案。而本题计算矩阵乘时是利用第三者点k来跳跃的,因此运算方式不再是一般的矩阵乘,而是取max之后再取min,代码如下:

查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=205;
const int INF=1e9;
int n,m,k,q,w;
struct pi{
    int a[N][N];
    pi(){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++)a[i][j]=INF;
        }
    }
};
pi operator*(pi x,pi y){
    pi t;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            for(int k=1;k<=n;k++){
                t.a[i][j]=min(t.a[i][j],max(x.a[i][k],y.a[k][j]));
            }
        }
    }
    return t;
}
pi pq(pi A,int k){
    pi res=A;
    k--;
    while(k){
        if(k&1)res=res*A;
        A=A*A;
        k>>=1;
    }
    return res;
}
void solve(){
    cin>>n>>m;
    pi A;
    for(int i=1;i<=m;i++){
        int u,v;cin>>u>>v;
        A.a[u][v]=0;
    }
    cin>>k;
    for(int i=1;i<=k;i++){
        int u,v;cin>>u>>v;
        A.a[u][v]=min(A.a[u][v],i);
    }
    cin>>q>>w;
    pi ans=pq(A,w);
    int st,ed;
    for(int i=1;i<=q;i++){
        cin>>st>>ed;
        int x=ans.a[st][ed];
        if(x!=INF)cout<<x<<'\n';
        else cout<<"-1\n";
    }
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)solve();
}

J题:(默认你已经会了Kruskal重构树)

简化题意:给出n个点和点权,每俩个点之间的边权为gcd(ai,aj),有q个询问,每个询问给出起点和终点,求所有路径中一条路径上边权最小值的最大值。

首先,在任意无向图中,两点之间所有路径的最小边权的最大值,等于该两点在最大生成树上唯一路径的最小边权。
这是因为Kruskal算法按边权从大到小添加边,首次将两点连通的那条边就是该瓶颈值。同样的,如果问题是求所有路径的最大边权的最小值,那么这个最小值一定是在该图的最小生成树上的。因此我们只需构造原图的最大生成树,然后回答树上路径最小值查询

对于一个完全图,如果直接枚举边会直接爆掉(因为点的数量是可以达到1e6次的,边数是可以达到1e12条的),因此此时的问题就变成了如何减少需要建立的边数,准确来说是有效边。此时我们就得通过边的性质来思考了,首先边权是等于gcd(au,av)的,我们如果假设其值为d,那么au和av实际上都是可以被d整除的,所以我们可以用一种缩点的思想:将所有点权能被d整除的点缩为一个点(联通块)。这里我举个具体的例子:
假设有4个城市,点权分别为:
6,15,10,30
最大点权mx=30。初始每个城市自成一个连通块,根就是自己:1,2,3,4。首先从d=30开始,会发现只有30可以被整除,因此只有一个根节点,不用合并;之后d=15,会发现15可以被整除,此时15和30就是俩个根节点,创建新节点(点权为15)并合并这俩个节点;之后d=10,发现10和30都可以被整除,出现了俩个根节点,合并(新节点点权10);最后d=6发现可以被30整除,再创建新节点(点权6)合并。因为每颗树的根节点一定是整颗树中数最小的值,也代表了整颗子树的gcd上限,此时这棵Kruskal树就可以通过lca得到最终结果。所以本题的巧妙之处就在于gcd条件的转化,通过枚举d的缩点思想来实现建树。这里给出建树代码:

for(int d=mx;d>=1;d--){
        vector<int> roots;
        for(int k=d;k<=mx;k+=d){
            for(int u:pos[k]){
                int r=dsu.find(u);
                if(vis[r]!=d){
                    vis[r]=d;
                    roots.push_back(r);
                }
            }
        }
        if(roots.size()>=2){
            val[++idx]=d;
            for(int r:roots){
                e[idx].push_back(r);
                dsu.fa[r]=idx;
            }
            dsu.fa[idx]=idx;
        }
    }

看到三个for循环我相信你一定会问:这时间复杂度能过?接下来我们来证明:

外层循环d从mx到1,最多1e6.内层枚举所有d的倍数,次数约为mx/d。因此枚举总次数为

                                                        image

这是调和级数的经典结果。对于每个倍数k,我们需要遍历pos[k]中的所有点。那么我们可以将其转化为一个点i会被访问多少次,实际上访问次数=i的因数个数。对于ai<=1e6,最多的约数数量大约240,平均100(网上会有一些结论和证明可以自行寻找),因此整体访问次数为n*100,最坏达到1e8次。

综上,时间复杂度为O(MlogM+nlogM)其中n,m都可为1e6,实际可以在3s时限内运行完成。

以下是完整代码:

查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int N=1e6+5;
int n,q;
int a[N*2],val[N*2],vis[N*2];
vector<int> pos[N*2],dep,e[N*2];
vector<vector<int>> up;
struct DSU{
    vector<int> fa;
    DSU(int n){
        fa.assign(n+1,0);
        for(int i=1;i<=n;i++)fa[i]=i;
    }
    int find(int x){
        return x==fa[x]?x:fa[x]=find(fa[x]);
    }
    void in(int a,int b){
        int x=find(a),y=find(b);
        if(x==y)return;
        fa[x]=y;
    }
};
void dfs(int u,int f){
    dep[u]=dep[f]+1;
    up[u][0]=f;
    for(int i=1;i<=20;i++)up[u][i]=up[up[u][i-1]][i-1];
    for(auto v:e[u])dfs(v,u);
}
int lca(int x,int y){
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)if(dep[up[x][i]]>=dep[y])x=up[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;i--){
        if(up[x][i]!=up[y][i]){
            x=up[x][i];
            y=up[y][i];
        }
    }
    return up[x][0];
}
void solve(){
    cin>>n>>q;
    int mx=-1;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        mx=max(mx,a[i]);
        pos[a[i]].push_back(i);
    }
    int idx=n;
    DSU dsu(n+N);
    for(int d=mx;d>=1;d--){
        vector<int> roots;
        for(int k=d;k<=mx;k+=d){
            for(int u:pos[k]){
                int r=dsu.find(u);
                if(vis[r]!=d){
                    vis[r]=d;
                    roots.push_back(r);
                }
            }
        }
        if(roots.size()>=2){
            val[++idx]=d;
            for(int r:roots){
                e[idx].push_back(r);
                dsu.fa[r]=idx;
            }
            dsu.fa[idx]=idx;
        }
    }
    dep.assign(idx+1,0);
    up.assign(idx+1,vector<int>(21,0));
    dfs(idx,0);
    for(int i=1;i<=q;i++){
        int x,y;cin>>x>>y;
        if(x==y)cout<<a[x]<<'\n';
        else cout<<val[lca(x,y)]<<'\n';
    }
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)solve();
}
/*
6 3
6 15 22 33 35 63
5 6
3 5
2 2
*/

 

posted on 2026-05-26 00:34  LeoCodex  阅读(22)  评论(0)    收藏  举报