Xcpc训练题目若干(一)

优先队列+贪心

题意:现有n个电池,每个电池容量为ai。有m个充电桩,充电桩位置为pi,能为ti电池充满电。可以任意选择电池进行消耗,一电池能行一公里,问最远路程

思路:

显然,消耗电池的顺序是按充电桩位置顺序排列的。因为不能回头,每个充电桩都要尽可能利用到

不妨使用优先队列,按充电桩优先级排列每个电池,(如果没有对应充电桩,说明该电池电量只能减少,所以把消耗的机会留给那些可能充上电的电池最好)

每次更新的时候把该电池的下一个充电桩位置当作新的优先级

void solve(){
    int n,m;cin>>n>>m;
    vector<int>a(n+1);rep(i,1,n)cin>>a[i];
    vector<int>at;at=a;
    vector<pii>b(m+1);rep(i,1,m){cin>>b[i].fi>>b[i].se;}

    priority_queue<pii,vector<pii>,greater<pii>>pq;
    vector<int>vis(n+1),idx(n+1);
    vector<vector<int>>g(n+1);

    rep(i,1,m){
        g[b[i].se].pb(b[i].fi);
        vis[b[i].se]=1;
    }

    rep(i,1,n){
        if(!vis[i]){
            pq.push({1e17,i});
        }else{
            pq.push({g[i][0],i});
        }
    }

    int now=0;
    int sum=0;
    rep(i,1,n)sum+=a[i];

    rep(i,1,m){
        int to=b[i].fi;
        int fresh = b[i].se;

        if(sum+now<to)break;

        int pos=0;
        while(pq.size()){
            auto[x,y]=pq.top();
            pos=x;
            if(now+a[y]>=to){
                sum-=(to-now);
                a[y]-=(to-now);
                now=to;
                break;
            }else{
                sum-=a[y];
                now+=a[y];
                a[y]=0;
                pq.pop();
            }
        }

        if(pos==to)pq.pop();
        if(now<to)break;

        sum +=(at[fresh]-a[fresh]);
        a[fresh] = at[fresh];
        idx[fresh]++;
        if(idx[fresh]<g[fresh].size())pq.push({g[fresh][idx[fresh]],fresh});
        else pq.push({1e17,fresh});
    }
    cout<<(now+sum)<<endl;
}

拓补DAG+连通块Dj

题意:给定一个既有无向边又有有向边的图,无向边边权一定为正,有向边边权可正可负,但保证若存在u->v这样的边,则一定不会存在从v到u的路径,给出原点s,求所有点到原点的距离

思路:
负权图使用SPFA的话,复杂度最坏\(O(nm)\),会TLE

考虑将无向图构成的连通块缩点,这样图变成一个连通块DAG

使用拓补排序将连通块之间的距离更新,每次取连接点到原点距离的最小值

使用Dj,将一个连通块的所有点和它到原点的距离入堆。

注意有一些无法到达点通过负边权可能使另外一些无法到达的点变得可达

代码方面,缩点使用一个DFS,再做一个点到连通块的映射,连通块有多少个点的数组即可

int n,m,p,s;
vector<pii>e[maxn];
vector<int>g[maxn];
int id[maxn];
int vis[maxn];
int cnt=0;
vector<int>k[maxn];
void dfs(int u){
    vis[u]=1;
    id[u]=cnt;
    k[cnt].pb(u);
    for(int v:g[u]){
        if(vis[v])continue;
        dfs(v);
    }
}
int d[maxn];
int ieg[maxn];
void solve(){
    cin>>n>>m>>p>>s;
    rep(i,1,m){
        int u,v,w;cin>>u>>v>>w;
        e[u].pb({v,w});
        e[v].pb({u,w});
        g[u].pb(v);
        g[v].pb(u);
    }
    rep(i,1,n){
        if(!vis[i]){
            cnt++;
            dfs(i);
        }
    }
    rep(i,1,n)vis[i]=0;
    rep(i,1,n)d[i]=1e16;
    d[s]=0;
    queue<int>q;

    rep(i,1,p){
        int u,v,w;cin>>u>>v>>w;
        e[u].pb({v,w});
        ieg[id[v]]++;
    }
    rep(i,1,cnt)if(!ieg[i])q.push(i);

    while(q.size()){
        int U = q.front();q.pop();

        priority_queue<pii,vector<pii>,greater<pii>>pq;
        for(auto ele:k[U])pq.push({d[ele],ele});
        while(pq.size()){
            auto[dist,u]=pq.top();
            pq.pop();
            if(vis[u])continue;
            vis[u]=1;
            for(auto[v,w]:e[u]){
                if(id[u]!=id[v]){
                    ieg[id[v]]--;
                    if(d[u]!=1e16)d[v]=min(d[v],d[u]+w);
                    if(ieg[id[v]]==0)q.push(id[v]);
                }else{
                    if(d[v]>d[u]+w){
                        d[v]=d[u]+w;
                        pq.push({d[v],v});
                    }
                }
            }
        }
    }
    rep(i,1,n){
        if(d[i]>=1e16){
            cout<<"NO PATH"<<endl;
        }else cout<<d[i]<<endl;
    }
}

线段树维护单调栈大小

题意:给定一个数组,有单点修改操作。维护一个单调递增斜率的单调栈,并求出大小

思路:
关键在于线段树的pull操作

发现要维护区间最大值单调递增,假设一个节点的左右区间已经维护好了前缀最大值长度以及最大值

由于从左到右的顺序,一定会选择左区间的最大值以及前缀最大值长度

  • 如果右区间最大值小于等于左区间最大值,那么右区间对答案无贡献
  • 否则,右区间上二分,如果右区间的左区间最大值小于左区间最大值,直接递归右区间的右区间,否则,答案是右区间的右区间比右区间的左区间最大值大的长度+递归处理左区间
  • 递归到叶子节点,如果叶子节点值大于左区间最大值则+1

注意:若叶子节点值为0则不算,右区间的右区间前缀最大值长度为tr[p].n-tr[ls].n,因为右区间的右区间的答案单独是没有比右区间的左区间最大值大前缀最大值长度的

#define ls p<<1
#define rs p<<1|1 
int n,m;
double h[maxn];
int C;
struct node{
    int l,r;
    double mx;
    int n;
}tr[maxn<<2];
int nums;

void bin(int p,double Max){
    int l=tr[p].l,r=tr[p].r;
    if(l==r){
        if(tr[p].mx>Max)nums++;
        return;
    }
    int mid=l+r>>1;
    if(tr[ls].mx<=Max)bin(rs,Max);
    else{
        nums+=tr[p].n-tr[ls].n;
        bin(ls,Max);
    }
}
void pull(int p){
    if(tr[ls].mx>=tr[rs].mx){
        tr[p].mx=tr[ls].mx;
        tr[p].n=tr[ls].n;
    }else{
        nums=0;
        tr[p].mx=tr[rs].mx;
        bin(rs,tr[ls].mx);
        tr[p].n=tr[ls].n+nums;
    }   
}
void build(int p,int l,int r){
    tr[p].l=l;tr[p].r=r;
    if(l==r){
        tr[p].mx = 1.0*h[l]/l;
        tr[p].n = 0;
        return;
    }
    int mid=l+r>>1;
    build(ls,l,mid);build(rs,mid+1,r);
    pull(p);
}
void change(int p,int x,int v){
    int l=tr[p].l,r=tr[p].r;
    if(l==r){
        tr[p].mx=1.0*v/l;
        if(v)tr[p].n=1;
        else tr[p].n=0;
        return;
    }
    int mid=l+r>>1;
    if(x<=mid)change(ls,x,v);
    else change(rs,x,v);
    pull(p);
}
void solve(){
    cin>>n>>m;
    build(1,1,n);
    while(m--){
        C++;
        int x,y;cin>>x>>y;
        change(1,x,y);
        cout<<tr[1].n<<endl;
    }
}

分组+DP

题意:给定一个序列,可每次使一个元素+1.求让所有长度为L的子数组为m的倍数的最小操作次数

思路:
需满足 a[i] = a[i+l](mod m)
构成L个剩余系
且最后所有剩余系值之和模m为0

int n,m,l;
vector<int>V[505];
int f[505][505];
int c[505][505];
void solve(){
    memset(f,inf,sizeof f);
    memset(c,inf,sizeof c);
    cin>>n>>m>>l;
    vector<int>a(n+1);rep(i,1,n)cin>>a[i];
    rep(i,1,n){
        V[i%l].pb(a[i]);
    }

    rep(i,1,l){
        for(int j=0;j<m;j++){
            int cnt=0;
            for(auto s:V[i-1]){
                if(s<=j){
                    cnt+=(j-s);
                }else{
                    //s>j
                    cnt+=(j-s+m);
                }
            }
            c[i][j]=cnt;
        }
    }
    f[0][0]=0;
    //定义f[i][j]:到第i组时,总和模m 等于 j 的最小操作次数
    rep(i,1,l){
        rep(z,0,m-1){
            rep(j,0,m-1){
                int lst = (z+j)%m;
                f[i][lst] = min(f[i][lst], f[i-1][z]+c[i][j]);
            }
        }
    }
    cout<<f[l][0]<<endl;
}

posted @ 2025-08-19 19:37  Marinaco  阅读(31)  评论(0)    收藏  举报
//雪花飘落效果