洛谷笔记 Day 3(2)

好难。

T1

P1631 序列合并

题意

有两个长度为 \(N\)单调不降序列 \(A,B\),在 \(A,B\) 中各取一个数相加可以得到 \(N^2\),求这个 \(N^2\) 个值中最小的 \(N\) 个。\(1 \le N \le 10^5\)\(1 \le a_i,b_i \le 10^9\)

题解

经典题,由于都有单调性,我们把相加的矩阵弄出来:

\(c_{i,j}=a_i+b_j\),对于任意 \(i,j\),都有 \(c_{i,j}\le c_{i+1,j},c_{i,j} \le c_{i,j+1}\),并且对于每行 \(c\),第一个绝对是最小的。

我们不妨做一个堆,先把每行一个个放进去,然后每次弹出最小的,然后接着取最小值所在行的下一个。

代码

坑点是数组开大一点。

#include<bits/stdc++.h>
using namespace std;
struct node{
    int v,x,y;
    bool operator<(const node &k)const{
        return v>k.v;
    }
};
priority_queue<node>q;
int a[100005],b[100005];
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;++i)cin>>a[i];
    for (int i=1;i<=n;++i)cin>>b[i];
    for (int i=1;i<=n;++i){
        q.push({a[i]+b[1],i,1});
    }
    while (n--){
        auto [v,x,y]=q.top();
        q.pop();
        cout<<v<<" ";
        q.push({a[x]+b[y+1],x,y+1});
    }
    
}

T2

P8755 [蓝桥杯 2021 省 AB2] 负载均衡

题解

一开始很难想到需要堆。但是发现去判一个东西是否能删,无非就是看算力和时间,我用一个,把结束时间最早的排在前面,这样每当对 \(b\) 有一个请求时,我们就把所有完成的弹出来,容易发现一定是堆顶的一个前缀是可行的,并且来算复杂度发现每个请求只会被处理一次,带一只 \(\log\),这个思想就有点像惰性删除了。

代码

#include<bits/stdc++.h>
using namespace std;
int v[200005];
struct node{
    int s,val,t;
    bool operator<(const node &k)const{
        return t>k.t; 
    }
};
priority_queue<node>q[200005];
int main(){
    int n,m;cin>>n>>m;
    for (int i=1;i<=n;++i)cin>>v[i];
    while (m--){
        int a,b,c,d;cin>>a>>b>>c>>d;
        while (!q[b].empty()){
            auto [s,val,t]=q[b].top();
            if (t>a)break;
            else q[b].pop(),v[b]+=val;            
        }
        if (v[b]>=d){
            q[b].push({a,d,a+c});
            v[b]-=d;
            cout<<v[b]<<'\n';
        }
        else {
            cout<<-1<<'\n';
        }
    }
}

T3

简单题。

P12085 [蓝桥杯 2023 省 B] 整数删除

题解

这个就是要真的上惰性修改了,发现这个把两边的 \(+1\) 是难点,先考虑怎么找到,首先一定不是 \(i+1,i-1\),因为你可能会把 \(i\) 两边的删了,那么我们就链表维护。接着我们考虑维护一个 \(tag\),发现这个 \(tag\) 是只会让数加不会让数减的,也就是只会让数里堆顶更远,所以我们考虑弹出栈的时候如果这个数身上有 \(tag\) 就加上 \(tag\) 重新丢进堆里,直到找到一个身上没有 \(tag\) 的数。

代码

long long成了大问题我好想你,和你在一起

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[500005];
int l[500005],r[500005];
struct node{
    long long v,x;
    bool operator<(const node &k)const{
        if (v==k.v) return x>k.x;
        return v>k.v;
    }
};
priority_queue<node>q;
signed main(){
    int n,k;cin>>n>>k;
    for (int i=1;i<=n;++i){
        cin>>a[i];
        l[i]=i-1;
        r[i]=i+1;
        q.push({a[i],i});
    }
    r[0]=1; l[n+1]=n;
    
    for (int i=1;i<=k;++i){
        while (q.top().v!=a[q.top().x]){
            q.pop();
        }
        auto [v,x]=q.top();q.pop();
        int ll=l[x],rr=r[x];
        if (ll>=1) {
            a[ll]+=v;
            q.push({a[ll],ll});
        }
        if (rr<=n) {
            a[rr]+=v;
            q.push({a[rr],rr});
        }
        a[x]=-1;
        r[ll]=rr;
        l[rr]=ll;
    }
    
    for (int i=1;i<=n;++i){
        if (a[i]!=-1) cout<<a[i]<<" ";
    }
}

T4

P12123 [蓝桥杯 2024 省 B 第二场] 传送阵

有一点点困难但不是很大的题。

题解

首先我们需要证明一个性质,根据传送关系建出来的图对于任意联通块都是一个环(除了单点)。这个证明是容易的,其实相当于只要证不会出现自环就行,因为 \(n\)\(n\) 边无自环且联通就是一个环,你考虑如果有自环,还要与别的点联通,也就是 \(i \to j,j \to j\),这在 \(a\) 数组里 \(j\) 就会出现两次,不满足 \(a\) 是排列的性质,证闭。

有了这个性质做题就简单了,我们先并查集预处理出所有的联通块,然后去枚举这个任意门安在那里,也就是找到 \(\max \{sz_{f_i}+sz{f_{i+1}}\}\),扫一遍即可。

代码

#include<bits/stdc++.h>
using namespace std;
int f[1000005];
int sz[1000005];  
int find(int x){
    if (f[x]==x)return x;
    return f[x]=find(f[x]);
}
void join(int x,int y){
    int fx=find(x),fy=find(y);
    if (fx!=fy){
        f[fx]=fy;
        sz[fy]+=sz[fx]; 
    }
}
int main(){
    int n;cin>>n;
    for (int i=1;i<=n;++i){
        f[i]=i;
        sz[i]=1;  
    }
    for (int i=1;i<=n;++i){
        int x;cin>>x;
        join(i,x);
    }
    
    int ans=0;
    for (int i=1;i<=n;++i){
        if (find(i)==i){
            ans=max(ans,sz[i]);
        }
    }
    for (int i=1;i<n;++i){
        int fa=find(i),fb=find(i+1);
        if (fa!=fb){
            ans=max(ans,sz[fa]+sz[fb]);
        }
    }
    
    cout<<ans;
    return 0;
}

T5

弯弯绕绕但不是很难的题。

P1892 [BalticOI 2003] 团伙

题解

与其说 \(i,j\) 是敌人,不如说 \(i\)\(j\) 的敌人是朋友,那么事情就变得容易起来了,扩展域并查集,\(f_{i}\) 表示 \(i\) 的朋友,\(f_{i+n}\) 表示 \(i\) 的敌人,直接合并即可。

这个题最坑的是统计答案,有点人就直接统计了 \(1\)~\(2n\) 以自己为根的,这显然是不对的,因为你考虑有的敌人节点根本没用上,你就给它算入答案了!\(1\)~\(n\) 中以自己为根的行不行?也是不对的,你考虑正确答案的根是可能在敌人节点身上的。你需要的是统计 \(1\)~\(n\) 的祖先然后去重,这样可以完美避开上面两种情况。

最后说一下,这题为什么不能用带权并查集,维护是好维护的,但是你如何统计答案呢?这是一个谜。

代码

需要注意,如果 \(i,j\) 是朋友,\(i\) 的敌人和 \(j\) 的敌人不一定是朋友。

#include<bits/stdc++.h>
using namespace std;
const int N=10005;
int f[N*2];
int find(int x){
    if (x==f[x])return x;
    else return f[x]=find(f[x]);
}
void join(int x,int y){
    f[find(x)]=find(y);
}
int main(){
    int n,m;cin>>n>>m;
    for (int i=1;i<=2*n;++i)f[i]=i;
    while (m--){
        char op;
        int p,q;cin>>op>>p>>q;
        if (op=='F'){
            join(p,q);   
        }
        else {
            join(p+n,q);   
            join(p,q+n); 
        }
    }
    set<int>st;
    for (int i=1;i<=n;++i){
        st.insert(find(i));
    }
    cout<<st.size();
    return 0;
}

T6

唐题。

P1525 [NOIP 2010 提高组] 关押罪犯

题意

给你若干对敌人,分在一个监狱就有 \(c_i\) 的影响,一共两所监狱,请你分配使得影响的最大值最小,并输出他。

题解

给人的直觉是二分,你也肯定可以这么做。然后考虑 \(check\),假设你需要 \(check\)\(x\) 的答案是否可行,也就是说如果 \(c_i>x\),这两个就要拆开,我们直接扩展域,\(i\)\(i\) 在 A 监狱,\(i+n\)\(i\) 在 B 监狱,最后但是当你发现这个冲突中的两个人已经被连上了,且不能让他们在一块,肯定就是不行了。

基于这个判可行的思路,我们考虑是否能去掉二分,先把冲突按照影响力从大到小排序,接着遍历,如果能和就尽可能的和,和不了直接输出答案,必然最优。

代码

#include<bits/stdc++.h>
using namespace std;
int fa[4000005];
int find(int x){
    if (fa[x]==x)return x;
    return fa[x]=find(fa[x]);
}
void join(int x,int y){
    fa[find(x)]=find(y);
}
struct edge{
    int x;
    int y;
    int z;
}a[2000005];
bool cmp(edge h,edge l){
    return h.z<l.z;
}
int main(){
    int n,m;
    cin>>n>>m;
    for (int i=1;i<=m;++i){
        cin>>a[i].x>>a[i].y>>a[i].z;    
    }
    sort(a+1,a+m+1,cmp);
    for (int i=1;i<=2*n;++i)fa[i]=i;
    for (int i=m;i>=1;--i){
        if (find(a[i].x)==find(a[i].y)){
            cout<<a[i].z<<'\n';
            return 0;
        }
        join(a[i].x,a[i].y+n);
        join(a[i].y,a[i].x+n);
    }
    cout<<0<<'\n';
}

T6

P1197 [JSOI2008] 星球大战

题意

一个连通图,每次查询删除一个点以及其连边,输出删完之后的联通块数,查询之间不独立。

题解

经典 trick 时光倒流,删除是不好做的,添加还是容易的,我离线下来,先跑出来最后图省那些点,并把这些点能连的就连了,接着从最后一个查询开始,如果查询 \(x\),就把 \(x\) 及其边建立起来,可以并查集维护。

代码是早年写的,不展示了。

T7

P12698 [KOI 2022 Round 2] 树与查询

有点恐怖的题,看了题解才会得。

题意

一棵树,每次查询给你一个点集,问你这些点中 \((u,v)\) 的个数。(\(u,v\) 在仅有给你这个点集的情况下依旧联通)

题解

我们不妨去考虑一个联通块的贡献,设它大小为 \(k\),贡献就是 \(C_{k}^2\),接着考虑去维护这个联通块了,由于树上两点 \(u,v\) 之间能联通当且仅当 \(u \to LCA(u,v),v \to LCA(u,v)\) 路径上的点都是全的,也就是只能通过父节点往上到 \(LCA\) 来联通,换成联通块也是一样的发现对于每个点,如果它和它的父亲都存在就用并查集把他们连上,如果不理解可以自己拿一棵树手玩一下。最后就是对于每个联通块算出它的大小并统计贡献了。

本题我认为难点在于清空,清空不到位就会 WA,不能只初始化不清空,也不能只清空不初始化。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=500005;
struct edge{
    int to,nxt;
}e[N*2];
int tot=0,hd[N],fa[N];
int f[N],cnt[N],s[N];
bool vis[N];
void add(int x,int y){
    e[++tot].to=y;
    e[tot].nxt=hd[x];
    hd[x]=tot;
}
void dfs(int u,int ff){
    fa[u]=ff;
    for (int i=hd[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (v==ff)continue;
        dfs(v,u);
    }
}
int find(int x){
    if (f[x]==x)return x;
    return f[x]=find(f[x]);
}
void join(int x,int y){
    f[find(x)]=find(y);
}
void solve(){
    int k;cin>>k;
    for (int i=1;i<=k;++i){
        cin>>s[i];
        f[s[i]]=s[i];
        cnt[s[i]]=1;
        vis[s[i]]=1;
    }
    for (int i=1;i<=k;++i){
        if (fa[s[i]]&& vis[fa[s[i]]]){
            join(s[i],fa[s[i]]);
        }
    }
    for (int i=1;i<=k;++i){
        cnt[s[i]]=0;
    }
    for (int i=1;i<=k;++i){
        cnt[find(s[i])]++;
    }
    int ans=0;
    for (int i=1;i<=k;++i){
        if (find(s[i])==s[i]){
            ans+=cnt[s[i]]*(cnt[s[i]]-1)/2;
        }
    }
    cout<<ans<<'\n';
    for (int i=1;i<=k;++i){
        vis[s[i]]=0;
        cnt[s[i]]=0;
        f[s[i]]=0;
        s[i]=0;
    }
}
signed main(){
    int n;cin>>n;
    for (int i=1;i<n;++i){
        int u,v;cin>>u>>v;
        add(u,v);add(v,u);
    }
    dfs(1,0);
    int q;cin>>q;
    while (q--)solve();
}

T8

难题。

P14870 [ICPC 2020 Yokohama R] To be Connected, or not to be, that is the Question

题解

我们称左部点为白点,右部点为黑点。

先考虑图联通的条件,就是至少要 \(n\) 个点,\(n-1\) 条边,那么就可以对最终的合法条件进行转化,有一个很显然的性质是最终联通块一定都是纯色的,联通块是不好刻画的,为了方便我们把每个联通块看成一个点,然后能连出边的条数就是黑点的个数与白点个数的 \(\min\),因为要两两一对。我们设最终有 \(a\) 个黑点,\(b\) 个白点,\(L\) 个白点联通块,\(R\) 个黑点联通块,必然要求 \(L+R-1\le min(a,b)\),意思就是边数至少要是点数的 \(n-1\)

设阈值为 \(k\),对于一条边 \((u,v)\) 的断裂条件就是 \((u,v)\) 异色,也就是 \(l_u,l_v\le k\) 都染成白点或 \(l_u,l_v>k\) 都染成黑点,合起来不好做,纯色的性质也在提醒我们,我们分开做,发现判断第一个式子只需要判断 \(\max(l_u,l_v) \le k\) 就行,第二个式子类似的,就等价于 \(min(l_u,l_v)>k\),我们可以直接把左边这个东西求出来然后排序,我说一个白点,黑点就按照这个做就好了:外面枚举 \(k\),然后再维护一个指针 \(p\),去扫一遍,并查集维护联通块数,注意 \(p\) 是单调不降的,复杂度只有一个并查集的 \(\log\),这样就可以求出来对于每个 \(k\) 有多少个白色联通块。

还有就是统计这个白点黑点数量的问题,直接对 \(l_i\) 做桶枚举 \(k\) 即可,其实就有点像前缀和了。或者说你把 \(l_i\) 排序什么双指针二分随便搞一下就行。

最后再枚举一遍 \(k\),用我们一开始推出来的式子判断就做完了。

代码

量不是很小,细节比较多。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct edge{
    int s,t,v;
}e1[N],e2[N];
bool cmp(edge x,edge y){
    return x.v<y.v;
}
int a[N],f[N],n,m,num[N],cnt[N],L[N],R[N];
int find(int x){
    if (f[x]==x)return x;
    else return f[x]=find(f[x]);
}
void join(int x,int y){
    f[find(x)]=find(y);
}
void init(){
    for (int i=1;i<=n;++i)f[i]=i;
}
int main(){
    cin>>n>>m;
    vector<int>v;
    for (int i=1;i<=n;++i){cin>>a[i];v.push_back(a[i]);}
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
    int sz=v.size();
    for (int i=1;i<=n;++i){
        a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1;
        num[a[i]]++;
    }
    for (int i=1;i<=sz;++i)cnt[i]=cnt[i-1]+num[i];
    
    for (int i=1;i<=m;++i){
        int u,vv;
        cin>>u>>vv;
        e1[i].s=u, e1[i].t=vv;
        e1[i].v=max(a[u], a[vv]);
        e2[i].s=u, e2[i].t=vv;
        e2[i].v=min(a[u], a[vv]);
    }
    
    sort(e1+1,e1+m+1,cmp);
    sort(e2+1,e2+m+1,cmp);
    
    init();
    int p=1,blk=0;
    for (int k=1;k<=sz;++k){
        blk+=num[k];
        L[k]=blk;
        while (p<=m && e1[p].v<=k){
            if (find(e1[p].s)!=find(e1[p].t)){
                blk--;
                L[k]--;
                join(e1[p].s,e1[p].t);
            }
            p++;
        }
    }
    
    init();
    p=m;
    blk=0;
    for (int k=sz;k>=1;--k){
        if (k<sz) blk+=num[k+1];
        R[k]=blk;
        while (p>=1 && e2[p].v>k){
            if (find(e2[p].s)!=find(e2[p].t)){
                blk--;
                R[k]--;
                join(e2[p].s,e2[p].t);
            }
            p--;
        }
    }
    
    int ans=-1;
    for (int k=1;k<=sz;++k){
        int lcnt=cnt[k];
        int rcnt=n-lcnt;
        if (lcnt==0 || rcnt==0) continue;
        if (L[k]+R[k]-1 <= min(lcnt, rcnt)){
            ans=v[k-1];
            break;
        }
    }
    
    cout<<ans;
    return 0;
}
posted @ 2026-02-09 17:50  Cefgskol  阅读(5)  评论(0)    收藏  举报