题目整理-1(练习1)
T1、Round Dance
题面描述:
有 \(n\) 个人围成了若干个圆圈,其中每一个圈的人数都大于等于 \(2\),给定这 \(n\) 个人每个人的相邻的人之一,求出满足此条件下这 \(n\) 个人组成圆圈的最大以及最小数量。
思路:
将每个人抽象化为一个点,则该点在组成圆圈的时候,有且仅有不同的两个点与它相连。对于每一个点,题目中给出了与它相连的一个点,我们给这两点建无向边,然后进行研究。
经过我们的推断,我们可以得出最大值就是联通块的数量,因为此时联通块内的点必定在同一环内,要使环的数量最多,我们就不能使任意两个联通块中的点(人)在同一个圆圈内,因此,最大值解决。
通过我们对样例以及观察推断,我们可以发现每个联通块要么是一个环,要么是一条链。因为一个人最多和两个人相邻,所以无法出现一个点(人)连三个及以上点数,因此,每个联通块只能使链和环。进一步想:
- 当联通块为环时,圆圈的形状已经确定,所以它必然是一个单独的圆圈。
- 当联通块为链时,两侧的端点处的点仅连了一条边,所以我们可以将所有的环的端点处首尾顺次相连,得到一个大环,此时值最小。
因此,设联通块中环的数量为 \(num_1\) ,链的数量为 \(num_2\),环(圆圈)的数量为 \(k\),则:
标签:
DFS
图论
代码实现
this
#include<bits/stdc++.h>
#define N 200005
using namespace std;
int T,n,a[N],b,vis[N],t[N];
int cnt,ans1,ans2;
set<int> g[N];
void dfs(int x,int fa){
vis[x]=1;
for(auto i:g[x]){
if(!vis[i]) dfs(i,x);
else if(i!=fa) b=1;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
while(T--){
ans1=ans2=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
g[i].insert(a[i]),g[a[i]].insert(i);
}
for(int i=1;i<=n;i++)
if(!vis[i]) dfs(i,-1),ans2++,ans1+=b,b=0;
for(int i=1;i<=n;i++) vis[i]=0,g[i].clear();
cout<<ans1+(ans1!=ans2)<<" "<<ans2<<"\n";
}
return 0;
}
T2、Path Queries
题面描述:
给你一个 \(n\) 个节点的树,并且给 \(m\) 个询问,每个询问包含一个数字 \(q\),求在这棵树中简单路径上边的最大值不超过 \(q\) 的方案数。
思路:
过程类似于最小生成树,但是原理不同。
先定义 \(ans_i\),表示在这棵树上简单路径上边的最大值为 \(i\) 的方案数。
过程:
- 对所有边按路径长度排序,再按照最小生成树的方式从按边权小到大加边。
- 在加边的同时,我们设这条边的权值为 \(w\),联通的两个联通块内的点数分别为 \(siz_i\) 和 \(siz_j\),则可以得到式子:
意思是在原本未联通的两个联通块中,每个点都可以与另外的一个联通块中的任意一点组成一条简单路径,且最大值为 \(w\)。
- 对 \(ans\) 数组求前缀和,\(ans_i\) 的意义就变为了这棵树中简单路径上边的最大值不超过 \(i\) 的方案数。
标签
图论
并查集
计数
前缀和
排序
代码实现:
this
#include<bits/stdc++.h>
#define N 200005
#define ll long long
using namespace std;
int n,m,fa[N],siz[N];
ll ans[N];
struct node{ int a,b,w; }e[N];
int cmp(node a,node b){ return a.w<b.w; }
int fd_fa(int x){ return ((fa[x]==x)?x:(fa[x]=fd_fa(fa[x]))); }
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;
for(int i=1;i<n;i++) cin>>e[i].a>>e[i].b>>e[i].w;
sort(e+1,e+n+1,cmp);
for(int i=1;i<=n;i++){
e[i].a=fd_fa(e[i].a),e[i].b=fd_fa(e[i].b);
ans[e[i].w]+=(ll)siz[e[i].a]*siz[e[i].b];
fa[e[i].b]=e[i].a,siz[e[i].a]+=siz[e[i].b];
}
for(int i=1;i<=2e5;i++) ans[i]+=ans[i-1];
while(m--){
cin>>n;
cout<<ans[n]<<" ";
}
return 0;
}
T3、Vlad and the Mountains
题面描述:
给你一个有 \(n\) 个点、\(m\) 条边的无向图,点 \(i\) 的点权为 \(h_i\)。若你的能量值为 \(e\),当你从点 \(i\) 到达点 \(j\) 时,则能量值变为 \(e-(h_j-h_i)\)。给定 \(q\) 个询问,每个询问形如 \(s\ t\ e\) ,表示询问初始能量值为 \(e\) ,能否从点 \(s\) 走到点 \(t\)。注:若能量值变为为负,则你将死去。
思路:
我们先推导一下当初始能量值为 \(e\) 时,从点 \(s\) 到达点 \(t\) 后所剩的能量值。
eg.
此时最终到达 \(t\) 时的能量值为:
所以,我们可以得出,对于这种计算最终能量值的方式,最终的能量值只与起点与终点的能量值有关!!!
好了,终于可以步入正轨了。在一条路径中,我们要求到达途中经过的每一个点时能量值都不能小于零,所以,为何不可理解为从起点出发到到达点 \(t\) 的路径上的每一个点最终的能量值都不可小于零呢?
完全可以,所以我们需要判断这个路径上的点 \(i\) 中最大的 \(h_i\),只要这个 \(h_i\) 比 \(e+h_s\) 小,就可以成功到达!
所以,我们直接一个操作,对于每个询问,直接把所有点权大于 \(e+h_s\) 的点一删,顺手判断一下 \(s\) 与 \(t\) 是否联通不就可以了吗?
好吧。。。这复杂度确实有点大了,考虑一下反着操作,既先将所有的操作按照 \(e+h_s\) 的大小从小到大拍个序,再依次加点以及可连的边,最后判断一下 \(s\) 与 \(t\) 是否联通,记录答案即可。
这个题终于完了
标签
图论
排序
代码实现
this
#include<bits/stdc++.h>
#define pr pair<int,int>
#define pr4 pair<pr,pr >
#define fr first
#define se second
#define N 200005
using namespace std;
int T,n,m,q,fa[N],ans[N],vis[N];
int tot,head[N];
pr a[N];
pr4 b[N];
struct edge{
int to,next;
void add(int u,int v){ to=v,next=head[u],head[u]=tot; }
}e[N<<1];
int fd_fa(int x){ return (fa[x]==x)?x:(fa[x]=fd_fa(fa[x])); }
int main(){
cin>>T;
while(T--){
tot=0;
memset(head,0,sizeof(head));
memset(ans,0,sizeof(ans));
memset(vis,0,sizeof(vis));
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i].fr,a[i].se=fa[i]=i;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
e[++tot].add(u,v);
e[++tot].add(v,u);
}
cin>>q;
for(int i=1;i<=q;i++){
cin>>b[i].se.fr>>b[i].se.se>>b[i].fr.fr;
b[i].fr.fr+=a[b[i].se.fr].fr;
b[i].fr.se=i;
}
sort(a+1,a+n+1);
sort(b+1,b+q+1);
tot=1;
for(int i=1;i<=q;i++){
while(tot<=n&&a[tot].fr<=b[i].fr.fr){
int faa=fd_fa(a[tot].se),fab;
vis[a[tot].se]=1;
for(int j=head[a[tot].se];j;j=e[j].next){
fab=fd_fa(e[j].to);
if(fab!=faa&&vis[e[j].to]) fa[fab]=faa;
}
tot++;
}
ans[b[i].fr.se]=(fd_fa(b[i].se.fr)==fd_fa(b[i].se.se));
}
for(int i=1;i<=q;i++) cout<<(ans[i]?"YES\n":"NO\n");
}
return 0;
}
T4、Information Graph
题面描述
在某公司中有 \(n\) 名员工,开始时员工之间没有任何关系,接下来会有 \(m\) 个操作。
-
\(y\) 成为了 \(x\) 的上司( \(x\) 在那之前不会有上司)
-
员工 \(x\) 得到了一份文件,然后 \(x\) 把文件传给了他的上司,然后上司又传给了他的上司,以此类推,直到某人没有上司,将文件销毁
-
询问 \(x\) 是否看过某份文件。
思路:
其实可以直接转换为雨天的尾巴这道题,具体过程如下:
-
记录操作 \(2\) 中 \(x\) 以及他/她的最终上司 \(y\),并且记录文件为 \(z\).
-
离线查询。
-
完美转换。
此时题面变为:
给你一个森林,有操作为:从 \(x\) 到 \(y\) 的路径上发放一袋 \(z\) 类型的物品,有查询为:查询点 \(x\) 上是否存在类型 \(z\) 的物品。
这不直接几乎原题了吗。
好了,所以我们使用线段树合并加上树上差分,完美解决此题!
标签
线段树合并
树上差分
代码实现:
this
#include<bits/stdc++.h>
#define pr3 pair<pair<int,int>,int>
#define pr pair<int,int>
#define mpr3(a,b,c) make_pair(make_pair(a,b),c)
#define mpr(a,b) make_pair(a,b)
#define fr first
#define se second
#define N 100005
#define ll long long
using namespace std;
struct x_tree{
#define M N<<5
int idx;
int sm[M],ls[M],rs[M];
#undef M
#define mid ((l+r)>>1)
void push_up(int id){ sm[id]=sm[ls[id]]+sm[rs[id]]; }
void add(int &id,int l,int r,int k,int s){
if(!id) id=++idx;
if(l==r){ sm[id]+=s;return; }
if(mid>=k) add(ls[id],l,mid,k,s);
else add(rs[id],mid+1,r,k,s);
push_up(id);
}
int sum(int id,int l,int r,int k){
if(!sm[id]) return 0;
if(l==r) return sm[id];
if(mid>=k) return sum(ls[id],l,mid,k);
return sum(rs[id],mid+1,r,k);
}
void he(int &id,int td,int l,int r){
if(!td) return;
if(!id){ id=td; return; }
sm[id]+=sm[td];
if(l==r) return;
he(ls[id],ls[td],l,mid);
he(rs[id],rs[td],mid+1,r);
}
}t;
int cnt,num,cnt1,n,m,fa[N],ans[N];
pr3 a[N];
int tot,head[N],vis[N],dad[N];
vector<pr> g[N];
struct edge{
int to,next;
void add(int u,int v){ to=v,next=head[u],head[u]=tot; }
}e[N];
int fd_fa(int x){ return (fa[x]==x)?x:(fa[x]=fd_fa(fa[x])); }
void dfs(int x){
vis[x]=1;
#define y e[i].to
for(int i=head[x];i;i=e[i].next){
dfs(y);
t.he(x,y,1,1e5);
}
for(auto h:g[x]){
ans[h.se]=t.sum(x,1,1e5,h.fr);
}
#undef y
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
t.idx=n;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int opt,x,y;
cin>>opt>>x;
if(opt!=2) cin>>y;
else ++num,a[++cnt]=mpr3(x,fd_fa(x),num);
if(opt==1) fa[x]=fd_fa(y),dad[x]=y,e[++tot].add(y,x);
if(opt==3) cnt1++,g[x].push_back(mpr(y,cnt1));
}
for(int i=1;i<=cnt;i++){
int x=a[i].fr.fr,y=a[i].fr.se,z=a[i].se;
t.add(x,1,1e5,z,1);
if(dad[y]) t.add(dad[y],1,1e5,z,-1);
}
for(int i=1;i<=n;i++){
if(!vis[i]) dfs(fd_fa(i));
}
for(int i=1;i<=cnt1;i++) cout<<(ans[i]?"YES\n":"NO\n");
return 0;
}
T5、白雪皑皑
题面描述:
给出一个长度为 \(n\) 的序列,有 \(m\) 次操作,第 \(i\) 次操作表示将 \((i\times p+q) \ mod \ n\) 到 \((i\times q+p) \ mod \ n\) 的序列中所有的值改为 \(n\)。输出所有操作后的序列。
思路:
淼。。。
第一眼:线段树板子题?!
第一次提交:怎么 \(T\) 一个点???
第 \(n\) 眼,哦~~,只需要最后的 \(n\) 次操作就行了!!!
第 \(n\) 次提交:过了。。。
易证
标签
线段树
数论
代码实现:
this
#include<bits/stdc++.h>
#define il inline
#define ll long long
#define N 1000005
using namespace std;
template<class T,ll count>
struct x_tree{
x_tree<T,count-1> s[2];
#define mid (1<<(count-1))
#define len (1<<count)
T sm,lz;
il x_tree(){ sm=lz=0; }
il void x_add(T k){ sm=k*len,lz=k; }
il void push_up(){ sm=s[0].sm+s[1].sm; }
il void push_down(){
if(!lz) return;
s[0].x_add(lz);
s[1].x_add(lz);
lz=0;
}
il void add(int l,int r,T k){
if(l<=1&&len<=r) x_add(k);
else{
push_down();
if(mid>=l) s[0].add(l,r,k);
if(mid<r) s[1].add(l-mid,r-mid,k);
push_up();
}
}
il T sum(int k){
push_down();
if(mid>=k) return s[0].sum(k);
return s[1].sum(k-mid);
}
#undef len
#undef mid
};
template<class T>
struct x_tree<T,0>{
T sm;
il x_tree(){ sm=0; }
il void x_add(T k){ sm=k; }
il void add(int l,int r,T k){ sm=k; }
il T sum(int k){ return sm; }
};
x_tree<int,20> t;
int n,m,p,q;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>p>>q;
for(int i=max(1,m-n+1);i<=m;i++){
int l=((ll)i*p%n+q)%n+1;
int r=((ll)i*q%n+p)%n+1;
if(l>r) swap(l,r);
t.add(l,r,i);
}
for(int i=1;i<=n;i++) cout<<t.sum(i)<<"\n";
return 0;
}
template
线段树在没有动态开点时
万岁!!!
注:
- 题号为本校OJ上的链接,题名为原出处链接。