牛客 导航系统
题面:
思路:
code:
//https://ac.nowcoder.com/acm/contest/3007/I
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=1e3+5;
struct Node{
int a,b,c;
}e[maxm*maxm];
int g[maxm][maxm];
int d[maxm][maxm];
int pre[maxm];
int ffind(int x){
return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
bool cmp(Node a,Node b){
return a.c<b.c;
}
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>g[i][j];
if(i!=j)d[i][j]=1e17;
}
}
int cnt=0;
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(g[i][j]!=g[j][i]){
cout<<"No"<<endl;
return 0;
}else{
e[++cnt]={i,j,g[i][j]};
}
}
}
sort(e+1,e+1+cnt,cmp);
for(int i=1;i<=n;i++){//init pre
pre[i]=i;
}
vector<int>ans;
for(int i=1;i<=cnt;i++){//kruskal
int a=e[i].a;
int b=e[i].b;
int x=ffind(a);
int y=ffind(b);
if(x!=y){
pre[x]=y;
d[a][b]=d[b][a]=e[i].c;
ans.push_back(e[i].c);
}
}
for(int k=1;k<=n;k++){//floyd
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(d[i][j]!=g[i][j]){
cout<<"No"<<endl;
return 0;
}
}
}
cout<<"Yes"<<endl;
sort(ans.begin(),ans.end());
for(int v:ans){
cout<<v<<endl;
}
return 0;
}
bzoj1001: [BeiJing2006]狼抓兔子
题面:
思路:
这类最小成本堵路问题要想到最小割。
根据最大流最小割定理可知答案就是最大流。
注意到是无向边,因此不需要建立反向的0边权虚边。
建图跑dinic计算最大流即可。
总结:用最小花费将原集合划分为两个集合这样的问题,试着想最小割
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=2e6+1000;
int head[maxm],nt[maxm<<2],to[maxm<<2],w[maxm<<2],cnt;
int d[maxm];
int n,m;
int st,ed;
queue<int>q;
void add(int x,int y,int z){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
while(!q.empty())q.pop();
q.push(st);
for(int i=st;i<=ed;i++){
d[i]=0;
}
d[st]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=nt[i]){
int v=to[i];
if(!d[v]&&w[i]){
d[v]=d[x]+1;
if(v==ed)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int x,int flow){
if(x==ed)return flow;
int res=flow;
for(int i=head[x];i;i=nt[i]){
int v=to[i];
if(w[i]&&d[v]==d[x]+1){
int k=dfs(v,min(res,w[i]));
if(!k)d[v]=-1;
res-=k;
w[i]-=k;
w[i^1]+=k;
}
if(!res)break;
}
return flow-res;
}
int dinic(){
int maxflow=0;
while(bfs()){
maxflow+=dfs(st,2e9);
}
return maxflow;
}
int getid(int i,int j){
return (i-1)*m+j;
}
void init(){
cnt=1;
}
signed main(){
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){//横向边
for(int j=1;j<m;j++){
int a=getid(i,j);
int b=getid(i,j+1);
int c;
scanf("%d",&c);
add(a,b,c);
add(b,a,c);
}
}
for(int i=1;i<n;i++){//纵向边
for(int j=1;j<=m;j++){
int a=getid(i,j);
int b=getid(i+1,j);
int c;
scanf("%d",&c);
add(a,b,c);
add(b,a,c);
}
}
for(int i=1;i<n;i++){//斜向边
for(int j=1;j<m;j++){
int a=getid(i,j);
int b=getid(i+1,j+1);
int c;
scanf("%d",&c);
add(a,b,c);
add(b,a,c);
}
}
st=getid(1,1);
ed=getid(n,m);
int ans=dinic();
printf("%d\n",ans);
return 0;
}
CodeForces1278 D. Segment Tree
题意:
给n个线段(L,R)
保证线段的端点只出现一次
如果线段i和线段j相交,则在i和j之间建立一条边
问最后建出的图是否是一棵树
数据范围:n<=5e5
思路:
先按左端点从小到大排序,这样遍历的时候就能保证当前的l大于之前的所有L了,
然后只需要在之前的点中找到R在当前的(L,R)中的点,然后建边就行了
但是遍历之前的所有点的话是O(n)的,这个地方可以用set维护,set自带的二分能很快找到大于等于当前L的位置
set里面存的是右端点R,但是我们要的是编号,因为题目保证端点不重复,所以用map将R映射成编号就行了(pair也行)
然后暴力建图就行了
为什么可以暴力呢?
因为题目要求是一颗树,最多就建立n-1条边,超过n-1则不成立(其实就是中途如果出现环则不成立)
可以用并查集判环,在最后还要用并查集判断是否只有一个连通块。
总结:
这种有点像二维偏序,先按左端点排序降维,然后set+自带二分就能找到满足条件的元素了。
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=5e5+5;
struct Node{
int l,r;
}e[maxm];
map<int,int>mark;
int pre[maxm];
set<int>s;
int ffind(int x){
return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
bool cmp(Node a,Node b){
return a.l<b.l;
}
signed main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)pre[i]=i;
for(int i=1;i<=n;i++){
scanf("%d%d",&e[i].l,&e[i].r);
mark[e[i].r]=i;//用右端点记录点的编号
}
sort(e+1,e+1+n,cmp);//按l从小到大排序
for(int i=1;i<=n;i++){//当前l大于等于set里面的所有l
auto it=s.lower_bound(e[i].l);//找set中r在当前l-r之间的
for(;it!=s.end();it++){
if(*it>e[i].r)break;
int x=ffind(mark[e[i].r]);
int y=ffind(mark[*it]);
if(x==y){//如果形成环则不满足
puts("NO");
return 0;
}else{
pre[x]=y;
}
}
s.insert(e[i].r);
}
int sum=0;
for(int i=1;i<=n;i++){
if(pre[i]==i)sum++;
}
if(sum!=1){
puts("NO");
}else{
puts("YES");
}
return 0;
}
CodeForces1139 C. Edgy Trees
题意:
给一棵n个顶点的树,和一个整数k。
树的每条边有黑白两种颜色。
现在要你构造一个长度为k的序列a,满足:
沿着树上两点间最短路a(1)走到a(2),然后走到a(3),一直走到a(k)
其中a(i)可以重复
如果走的过程中至少走过一条黑边,那么就满足条件。
问有多少种这样的序列,答案对1e9+7取模
思路:
直接算好难算,考虑正难则反。
用序列总数量减去不经过白边的数量。
用并查集将白边连接的点合并为一个块,
遍历每个块,假设当前块有x个点,那么不经过黑色点的序列就有xk,因为每个位置有x种选择。
累加每个块的答案,用从数量nk减去即可。
code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=2e5+5;
const int mod=1e9+7;
int pre[maxm];
int cnt[maxm];
int n,k;
int ppow(int a,int b,int mod){
int ans=1;
while(b){
if(b&1)ans=ans*a%mod;
a=a*a%mod;
b>>=1;
}
return ans;
}
int ffind(int x){
return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
pre[i]=i;
cnt[i]=1;
}
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
if(c==0){
int x=ffind(a);
int y=ffind(b);
if(x!=y){
pre[x]=y;
cnt[y]+=cnt[x];
}
}
}
int ans=ppow(n,k,mod);
for(int i=1;i<=n;i++){
if(pre[i]==i){
ans=(ans-ppow(cnt[i],k,mod)+mod)%mod;
}
}
cout<<ans<<endl;
return 0;
}
CodeForces1139 E. Maximize Mex
题意:
n个学生m个俱乐部,每个学生有能力值p(i)和报名的俱乐部c(i)
接下来的d天,每天教练会删掉一个学生k(i)
然后在每一个俱乐部里面选出一位俱乐部内的成员,
这天的团队能力值是选出的成员的能力值的MEX
问每天的团队能力值最大是多少。
思路:
学生和俱乐部是二分图,用学生的能力值p(i)和c(i)建边
从mex=0开始跑二分图匹配,一直跑到无法匹配位置,答案就是无法匹配的mex
如果每天都删边重建图,然后重新匹配,复杂度过大,
逆向思维,从后往前计算,就变成了每次添加一条边,
添加完边之后直接继续跑二分图匹配。不需要再从0开始,从后往前的答案是单调不降的
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=5e3+5;
int head[maxm],nt[maxm],to[maxm],cnt;
int p[maxm],c[maxm];
int mark[maxm];
int now[maxm];
int ans[maxm];
int k[maxm];
int n,m;
int d;
void add(int x,int y){
cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;
}
int dfs(int x){
for(int i=head[x];i;i=nt[i]){
int v=to[i];
if(!mark[v]){
mark[v]=1;
if(now[v]==-1||dfs(now[v])){
now[v]=x;
return 1;
}
}
}
return 0;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>p[i];
for(int i=1;i<=n;i++)cin>>c[i];
cin>>d;
for(int i=1;i<=d;i++){
cin>>k[i];
mark[k[i]]=1;
}
for(int i=1;i<=n;i++){
if(!mark[i]){
add(p[i],c[i]);
}
}
memset(now,-1,sizeof now);
int last=0;
for(int i=d;i>=1;i--){
while(1){
memset(mark,0,sizeof mark);
if(dfs(last)){
last++;
}else{
break;
}
}
ans[i]=last;
add(p[k[i]],c[k[i]]);
}
for(int i=1;i<=d;i++){
cout<<ans[i]<<endl;
}
return 0;
}
CodeForces1328 E. Tree Queries
题意:
给一颗n个顶点的数,其中顶点1为根
m次询问,每次询问给出k个点,问你:
是否存在一个点x,满足这k个点与1到x这条路径的距离小于等于1,即k在路径上或者k与路径的距离为1
如果存在输出yes,否则输出no
思路:
假设s是这k个点中的一个点,因为需要满足s到路径的距离小于等于1,那么s要么在路径上要么与路径只有一条边。
发现两种情况下x的父节点都一定在路径上。
因此可以把k个点全部替换为父节点,那么新的k个点一定在一条链上。
如何判断是否是一条链呢,将这k个新点按深度从小到大排序,深度小的点一定是深度大的点的祖先,
对于相邻的新点x和y,判断lca(x,y)是否等于x,如果某一次不满足,则不成立。
单次查询的复杂度为O(klogn)
ps:
判断祖先关系也可以使用dfs序。
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=2e5+5;
vector<int>g[maxm];
int f[maxm][20],maxd;
int d[maxm];
int n,m;
void dfs(int x){
for(int v:g[x]){
if(!d[v]){
d[v]=d[x]+1;
f[v][0]=x;
dfs(v);
}
}
}
int lca(int a,int b){
if(d[a]<d[b])swap(a,b);
for(int i=maxd;i>=0;i--){
if(d[f[a][i]]>=d[b]){
a=f[a][i];
}
}
if(a==b)return a;
for(int i=maxd;i>=0;i--){
if(f[a][i]!=f[b][i]){
a=f[a][i],b=f[b][i];
}
}
return f[a][0];
}
bool cmp(int a,int b){
return d[a]<d[b];
}
signed main(){
scanf("%d%d",&n,&m);
maxd=log(n)/log(2)+1;
for(int i=1;i<n;i++){
int a,b;
scanf("%d%d",&a,&b);
g[a].push_back(b);
g[b].push_back(a);
}
d[1]=1;
f[1][0]=1;
dfs(1);
for(int j=1;j<=maxd;j++){
for(int i=1;i<=n;i++){
f[i][j]=f[f[i][j-1]][j-1];
}
}
while(m--){
int k;
scanf("%d",&k);
vector<int>temp;
for(int i=0;i<k;i++){
int x;
scanf("%d",&x);
temp.push_back(f[x][0]);
}
sort(temp.begin(),temp.end(),cmp);
int ok=1;
for(int i=1;i<k;i++){
int x=temp[i-1],y=temp[i];
if(lca(x,y)!=x){
ok=0;
break;
}
}
if(ok)puts("YES");
else puts("NO");
}
return 0;
}