(集训模拟赛2)抢掠计划(tarjan强)
题目:给你n个点,m条边的图,每个点有点权,有一些点是“酒吧”点,终点只能在“酒吧”,起点给定,路可以重复经过,但点权只能加一次,求最大的结果。
例如这个图,双实线表示是酒吧,结果呢是1->2->4->1->2->3->5所得值。
输入格式:
第一行N,M,下面M行是边,下面N行是点权,下面1行是起点与酒吧数量,下面一行是“酒吧”点的编号。
思路:
注意到:(边可以重复走,而点权只算一遍)这个条件,说明只要走到了一个环中的一个点,这个环里面所有点就一定都能走到,因为你可以走一圈回到入环的起点。
这是什么呢?这是名为“缩点”的高级技巧在呼唤!
我们可以把所有环看作一个点,权值是环内所有点权之和,只要环中有一个点是“酒吧”,那这个大环就可以看作一个“酒吧”,然后从起点所在“大点”开始,跑一遍单源最短路,找最大的路径长度即可(spfa最长路,dfs硬搜会被卡(搜,就硬搜))
代码:
#include#include #include #include #include using namespace std; const int maxn=5e5+10; struct E{int from,to,next;}edge[maxn]; E edge2[maxn];int head2[maxn],tot2; int head[maxn],tot; void add(int from,int to){ edge[++tot].from=from; edge[tot].to=to; edge[tot].next=head[from]; head[from]=tot; } void add2(int from,int to){ edge2[++tot2].to=to; edge2[tot2].next=head2[from]; head2[from]=tot2; } int dfn[maxn],vis[maxn],low[maxn]; int sta[maxn],top,Time; int belong[maxn],belongcnt,size[maxn]; int val[maxn],drink[maxn],drink2[maxn]; void tarjan(int u){ if(dfn[u])return; low[u]=dfn[u]=++Time; vis[u]=1;sta[++top]=u; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]); }else if(vis[v]){ low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]){ belongcnt++; while(sta[top+1]!=u){ belong[sta[top]]=belongcnt; size[belongcnt]+=val[sta[top]]; vis[sta[top]]=0; if(drink[sta[top]])drink2[belong[sta[top]]]=true; top--; } } } int viss[maxn],diss[maxn]; void spfa(int s){ queue<int> q; viss[s]=1;diss[s]=size[s]; q.push(s); while(!q.empty()){ int u=q.front();q.pop();viss[u]=0; for(int i=head2[u];i;i=edge2[i].next){ int v=edge2[i].to; if(diss[v] size[v]){ diss[v]=diss[u]+size[v]; if(!viss[v]){ viss[v]=1; q.push(v); } } } } } int main(){ int m,n; scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ int from,to; scanf("%d%d",&from,&to); add(from,to); } for(int i=1;i<=n;i++){ scanf("%d",&val[i]); } int begin,dnum; scanf("%d%d",&begin,&dnum); for(int i=1;i<=dnum;i++){ int x; scanf("%d",&x); drink[x]=true; } for(int i=1;i<=n;i++){ tarjan(i); } for(int i=1;i<=m;i++){ if(belong[edge[i].from]!=belong[edge[i].to]){ add2(belong[edge[i].from],belong[edge[i].to]); } } //缩完后点之间的边 spfa(belong[begin]); int ans=0; for(int i=1;i<=belongcnt;i++){ if(drink2[i])ans=max(ans,diss[i]);//只有是酒吧才算最大值 } printf("%d",ans); return 0; }
(集训模拟赛3)清理牛棚(思维最短路)
题目:
简而言之,就是给你i个牛,每个牛可以清扫M到E,代价为S,问覆盖全部区间的最小代价(这不显然是线段树板子吗)
分析:
我们可以这么想,如果一头牛从i打扫到j,那么就从i到j+1建一条边(题目中说了牛打扫的是闭区间,所以我们建边时候要处理一下,把[i,j]变成[i,j+1)否则加边区间会重复)
然后呢,我们对于每一个i点,都建一条权值为0的i到i-1的边,然后从起点到终点跑最短路。
这是为什么呢?
我们想一想,如果有两头牛,他们分别打扫1->5,2->6,如果不建反向的权值为0的边,那么这个情况下1和6是不联通的,我们需要解决这种有重叠区间的问题的话,只要从5到2建一条权值为0的边,这样在跑最短路跑到5的时候,可以回到2继续跑。所以,我们的方案就是通过反向建0的边,使本来不联通的“重叠”区间也可以连起来,而且这样不会影响最后结果,之前不联通,这样操作还是不联通,因为反向建的边是单向的,只能从较后位置到较前位置。
代码:
#includeusing namespace std; const int maxn=1e6+10; struct E{ int to,val,next; }edge[maxn]; int head[maxn],tot; void add(int from,int to,int val){ edge[++tot].to=to; edge[tot].val=val; edge[tot].next=head[from]; head[from]=tot; } int n,m,e; int vis[maxn],d[maxn]; void spfa(int s){ memset(d,0x3f,sizeof(d)); queue<int> q; d[s]=0;vis[s]=1;q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); vis[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(vis[v])continue; if(d[v]>d[u]+edge[i].val){ d[v]=d[u]+edge[i].val; q.push(v); vis[v]=1; } } } } int main(){ scanf("%d%d%d",&n,&m,&e); for(int i=m;i<=e;i++){ add(i+1,i,0); } for(int i=1;i<=n;i++){ int from,to,val; scanf("%d%d%d",&from,&to,&val); to++; add(from,to,val); } spfa(m); if(d[e+1]==0x3f3f3f3f){ printf("-1"); return 0; } printf("%d",d[e+1]);//注意最后的区间变成了[m,e+1)而不是[m,e]了 return 0; }
(集训模拟赛4)浇水(思维最短路)
题目:
其实这道题是个贪心
分析:
我们可以这样考虑:每一个喷射装置覆盖一个圆形的面积,但是如果喷射半径小于m/2,那这个喷头相当于废了,它连自己的上下都喷不到,就不可能选它了,接着,面积什么的显然不好处理,还会有一些重叠就更不好了,我们可以把每个喷头所覆盖的n上面长度作为该喷头的“有效范围”,由于上下对称性,只要长方形的一条长被覆盖满了,另外一条必覆盖满,所以我们把这道题看作有n个喷头,每个喷头覆盖l到r,求覆盖所有区间的最小数量。
emm,这句话怎么这么熟悉?看了一下上一道题的描述(显然这两道题是一道题)
所以打出代码来,也跟上一道题是一样的,我就不放代码了
显然还是有一些区别的,比如这道题每一个喷头覆盖区间的左右端点是doube类型,不能直接当结点,会有蛋疼的精度问题,所以……
我们直接把每一个double值扩大一个倍数转成整形,相应的n也扩大,这样就可以代入上一道题的代码了。(×5就可以)
附上代码:
#includeusing namespace std; const int maxn=1e7+10; int k,n,m; struct E{ int to,val,next; }edge[maxn]; int head[maxn],tot; void add(int from,int to,int val){ edge[++tot].to=to; edge[tot].val=val; edge[tot].next=head[from]; head[from]=tot; } int vis[maxn],d[maxn]; void spfa(int s){ memset(d,0x3f,sizeof(d)); queue<int> q; d[s]=0;vis[s]=1;q.push(s); while(!q.empty()){ int u=q.front(); //printf("%d ",u); q.pop(); vis[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(vis[v])continue; if(d[v]>d[u]+edge[i].val){ d[v]=d[u]+edge[i].val; q.push(v); vis[v]=1; } } } } int main(){ scanf("%d%d%d",&k,&n,&m); for(int i=1;i<=k;i++){ int aa,r; scanf("%d%d",&aa,&r); if(r<=m/2)continue; int ll=(int)(aa*5-sqrt(r*r-m*m/4)*5); int rr=(int)(aa*5+sqrt(r*r-m*m/4)*5); //区间变为:[当前位置×5-向左的距离×5,当前位置×5+向右的距离×5+1); //因为该区间相当于这个喷头覆盖范围的一个弦,所以左右延伸的距离(半弦长)=根号(半径平方-弦心距平方)/2; if(ll<0)ll=0; add(ll,rr+1,1); } for(int i=1;i<=n*25;i++)add(i,i-1,0); spfa(0); if(d[n*5+1]==0x3f3f3f3f)d[n*5+1]=-1; printf("%d",d[n*5+1]); return 0; }
(集训模拟赛8)升降梯上(思维最短路)
(集训模拟赛8)升降梯上(分层图最短路)
题目大意:
有n层楼,你现在在第1层,有一个电梯,上面有个拉杆,有m个控制槽,槽上有数字,拨到哪个槽就上升相应层数(不能下降到<=0或上升到>n层),最开始拉杆在“0”槽位处,数字有正有负,且控制槽之间的数字是有顺序的,每移动一格控制槽需要1s,每上或下一层楼要2s,问走到顶楼的最小时间。
分析:(看起来是个dp,好像也可以推出来转移方程,但是会有一些小问题,这边只考虑正解(最短路)。)
我们把每种需要花费时间的操作当成边来处理,时间就是边的权值。
我们可以把每一层的每一个控制槽看作一个结点,它向本层的其它槽位的点建边(因为这需要话费时间),还向它指向的那一层的这个槽建一条边(同上),权值按照题目要求设定。(注意:二维的点(i,j)不适合作为图的结点,我们可以把每一个点的坐标处理一下,变成一维的点,然后剩下的操作就好处理了。)
主要步骤:
1.转点,第一层的点从1到m,第二层的点是m+1到2*m……第n层的点是(n-1)*m+1到n*m。
2.建边,每一个点向周围的槽位和自己指向的槽位建边。
3.最短路,需要从第一层的“0”槽到顶层的槽位中找最小值。
附上代码:
#includeusing namespace std; const int maxn=1e6+10; struct E{ int from; int to,val,next; }edge[maxn]; int head[maxn],tot,dis[maxn],vis[maxn]; void add(int from,int to,int val){ edge[++tot].to=to; edge[tot].from=from; edge[tot].val=val; edge[tot].next=head[from]; head[from]=tot; } void spfa(int s){ memset(dis,0x3f,sizeof(dis));memset(vis,0,sizeof(vis)); dis[s]=0;vis[s]=1; queue<int> q; q.push(s); while(!q.empty()){ int u=q.front();q.pop();vis[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(dis[v]>dis[u]+edge[i].val){ dis[v]=dis[u]+edge[i].val; if(!vis[v]){ q.push(v);vis[v]=1; } } } } } int n,m,c[maxn]; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;i++)scanf("%d",&c[i]);//每一个槽位及上面的数字 //建同一层之间的边 for(int now=1;now<=n;now++){ for(int i=1;i<=m;i++){ for(int d=0;d<=m-1;d++){ if(i+d<=m)add((now-1)*m+i,(now-1)*m+i+d,d); if(i-d>=1)add((now-1)*m+i,(now-1)*m+i-d,d); } } } //建层与层之间的边 for(int now=1;now<=n;now++){ for(int i=1;i<=m;i++){ if((now-1+c[i])*m+i<=n*m&&(now-1+c[i])*m+i>=1){//边界条件:不超过最大节点(n*m)不小于最小节点(1) if(c[i]==0)continue; add((now-1)*m+i,(now-1+c[i])*m+i,2*abs(c[i])); } } } int start=0; for(int i=1;i<=m;i++){ if(c[i]==0)start=i; } spfa(start); int Min=0x7fffffff; for(int i=1;i<=m;i++){ Min=min(Min,dis[(n-1)*m+i]); } if(Min==0x3f3f3f3f)Min=-1; printf("%d",Min); return 0; }
(集训模拟赛9)最小环(思维最短路)
题目:
思路:
这道题要求求已知起点的一条最小环,边是无向的但每条边又只能走一次(一看到最小环不就应该知道是最短路了吗)
一般求最小环都是用floyd算法,详情请见老姚博客:https://www.cnblogs.com/hbhszxyb/p/12770720.html
这道题显然n3的效率会炸,所以我们需要另寻它法。
我们知道,一个简单环,断掉一条边就会形成一条链,我们可以利用这一点,尝试断掉某个点与起点相连的一条边,再求起点到它的最短路,那么到这个点的最短路+断掉的这条边权就是起点与这个点所在的环的大小了,我们枚举每一条与起点相连的边,并尝试断掉它,然后求环的大小,取最小值即可。
注意:每次断边要断两个,建议在建边时候按^1的方法去建(0、1是一对反向边,2、3是一对反向边,4、5是一对……),这样在断边时候好处理
附上代码:
#includeusing namespace std; const int maxn=4e5+10; struct E{int to,next,val;}edge[maxn]; int head[maxn],tot,Min=0x3f3f3f3f; void add(int from,int to,int val){ edge[tot].to=to; edge[tot].val=val; edge[tot].next=head[from]; head[from]=tot++; } int d[maxn],vis[maxn]; void spfa(int s){ memset(d,0x3f,sizeof(d)); memset(vis,0,sizeof(vis)); queue<int> q; d[s]=0; q.push(s); while(!q.empty()){ int u=q.front();q.pop();vis[u]=0; for(int i=head[u];~i;i=edge[i].next){ int v=edge[i].to; if(d[v]>d[u]+edge[i].val){ d[v]=d[u]+edge[i].val; if(!vis[v]){ q.push(v); vis[v]=1; } } } } } int n,m,t,from,to,val; int main(){ scanf("%d",&t); while(t--){ memset(head,-1,sizeof(head));tot=0; scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ scanf("%d%d%d",&from,&to,&val); add(from,to,val); add(to,from,val); } Min=0x3f3f3f3f; for(int i=head[1];~i;i=edge[i].next){ int now=edge[i].val;//断掉这条边的边权 int v=edge[i].to;//某个与起点直接相连的点 edge[i].val=edge[i^1].val=0x3f3f3f3f;//断边(给它恢复初始值) spfa(1); Min=min(Min,d[v]+now); edge[i].val=edge[i^1].val=now;//再建回来 } if(Min==0x3f3f3f3f)Min=-1; printf("%d\n",Min); } return 0; }
(集训模拟赛10)虫洞(分层图最短路)
题目描述
输入格式
思路:
这道题乍一看是一个最短路,但是它有一些特殊限制,我们就考虑分层图。
何谓分层图?
就是在同一个图里面不太好找出所有情况时,开几个一样的图,在图之间进行那些“特殊操作”(例如边权改为0,边权减半,反向走……),这样不打破原来图的结构,还能更方便快捷的求出结果。
这道题,我们就可以把双数时间点的图和单数时间点的图分开来,建成两个图,因为单数时间和双数时间的黑、白洞情况不同,其他的差别也只在这两个图之间发生。
这道题有几个难处理的地方:
1、一个洞跳到另一个洞后,所有洞的颜色都会改变,我们可以通过再两个图之间建边这个问题,例如样例:
1 2 3 4(洞编号)(黑洞为1,白洞为0)
1 0 1 0(偶数时间状态)
0 1 0 1(奇数时间状态)
我们假如要在3->4这一方向建边,我们就可以让上面图的3号指向下面图的4号,下面图的3号指向上面图的4号,因为从3号到4号转移过后,4号的状态会改变,我们要以4号的新状态再进行之后的操作,所以直接从3号建一条边到改变后的4号,接下来就从改变后的4号再向后进行操作即可。
2.同一个洞可以选择停留,还会消耗1个时间,我们可以在两个图相对应的两个点之间建边解决这个问题。如上图中的1号与下图中的1号,他们之间就满足状态改变,位置不变。
主要思路就是这些,下面附上代码
#includeusing namespace std; const int maxn=100010; int n,m,w[maxn],now[maxn],s; struct E{int to,next,val;}edge[maxn*6]; int head[maxn],tot; void add(int from,int to,int val){ edge[++tot].to=to; edge[tot].val=val; edge[tot].next=head[from]; head[from]=tot; } int dis[maxn],vis[maxn]; void spfa(int s){ memset(dis,0x3f,sizeof(dis)); queue<int> q; dis[s]=0;q.push(s); while(!q.empty()){ int u=q.front();q.pop();vis[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(dis[v]>dis[u]+edge[i].val){ dis[v]=dis[u]+edge[i].val; if(!vis[v]){ q.push(v); vis[v]=1; } } } } } void init(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",&now[i]); } for(int i=1;i<=n;i++){ scanf("%d",&w[i]); } for(int i=1;i<=n;i++){ scanf("%d",&s); if(now[i]){//如果是黑洞,那么由黑到白要花费s[i]。(这里now[i]表示偶数时间内的状态) add(i,i+n,s); add(i+n,i,0); }else{//如果是白洞,那么由白到黑花费为0。(这两种情况都要建双向边,当边反过来,黑到白也变成了白到黑) add(i,i+n,0); add(i+n,i,s); } } for(int i=1;i<=m;i++){ int from,to,val; scanf("%d%d%d",&from,&to,&val); if(now[from]==now[to]){ add(from,to+n,val); add(from+n,to,val); //两洞无论什么时候状态都一样,就不用考虑题目中delta的问题 }else{ if(now[from]==1&&now[to]==0){ add(from,to+n,val+abs(w[from]-w[to])); int x=max(0,val-abs(w[from]-w[to])); add(from+n,to,x); //前黑后白 }else if(now[from]==0&&now[to]==1){ add(from+n,to,val+abs(w[from]-w[to])); int x=max(0,val-abs(w[from]-w[to])); add(from,to+n,x); //前白后黑 } } } } int main(){ init(); spfa(1); printf("%d",min(dis[n],dis[2*n]));//两个n结点选最小值 return 0; }
(集训模拟赛12)道路和航线(奇怪的最短路)(洛谷P3008)
这道题显然就是一个最短路,但是这个最短路还不能乱跑:
题目中数据范围边数50000,显然nlogn是可以跑过,但是这道题有负权边,没办法直接跑Dij,怎么办呢(不要跟我说SPFA,它已经死了)
所以这道题我们要通过一些奇奇怪怪的方法,让他能跑Dij。(就是缩点+拓扑排序啦)
分析:
根据题目条件,我们知道道路是双向的且没有负权,航线是单向的但不存在于环中(没有一条航线可以通过其他道路或航线回来)。
我们可以对于每一堆双向的道路们,把它们缩在一起成为一个“联通块”,这样我们就可以在联通块内部跑Dij了,因为没有负权。
对于联通块之间的边,它们只能是航线,而且满足联通块之间没有环,那么:
我们可以通过跑拓扑序的方式来遍历每一个联通块。
正确性?
对于Dij来说,最开始除了起点,其它点的距离都被我们认为是无穷大,那么除了包含起点的联通块,其他联通块里所有点的值都是无穷大,对于这些块与起点块的关系有下面几种:
1.该联通块有一条航线指向起点块,那么由于航线的性质,该联通块内的点相对于起点来说肯定是“不可到达”的,那先遍历它也没什么问题。
2.该联通块被起点块指向,那么这个联通块内至少有一个点会被起点块中的点更新,那么它一定要排在起点块后遍历才能做到更新其内点的值不是无穷大。
3.类似上面的情况,只有所有指向一个块的块们都遍历过了,才会把这个块上该更新的点都更新过了,而且这样跑完之后,一定不会再有别的航线更新这个块了,这么遍历不就是拓扑序遍历吗!(先找入度为0的块,每遍历一条航线,那这条航线指向的块入度--,如果入度为0了,再遍历这个点)
for(int i=1;i<=p;i++){ int from,to,val; scanf("%d%d%d",&from,&to,&val); add(from,to,val); indegree[belong[to]]++; //这是在读入每一条航线的时候就处理入度了,belong[to]表示to所在的联通块 } for(int i=1;i<=belongcnt;i++){ if(indegree[i]==0)qq.push(i); }//qq是储存入度为0的联通块的队列 while(!qq.empty()){ int x=qq.front();qq.pop(); Dij(x); } //在Dij里面我们会处理减入度的情况 void Dij(){ if(belong[to]!=x){ indegree[belong[to]]--; if(indegree[belong[v]]==0)qq.push(belong[v]); } }