洛谷笔记 Day 3(2)
好难。
T1
题意
有两个长度为 \(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
题解
一开始很难想到需要堆。但是发现去判一个东西是否能删,无非就是看算力和时间,我用一个,把结束时间最早的排在前面,这样每当对 \(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
简单题。
题解
这个就是要真的上惰性修改了,发现这个把两边的 \(+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
有一点点困难但不是很大的题。
题解
首先我们需要证明一个性质,根据传送关系建出来的图对于任意联通块都是一个环(除了单点)。这个证明是容易的,其实相当于只要证不会出现自环就行,因为 \(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
弯弯绕绕但不是很难的题。
题解
与其说 \(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
唐题。
题意
给你若干对敌人,分在一个监狱就有 \(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
题意
一个连通图,每次查询删除一个点以及其连边,输出删完之后的联通块数,查询之间不独立。
题解
经典 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;
}

浙公网安备 33010602011771号