一、大纲
本周作业与实验题目如下:
- 区间选点II(差分约束系统)
- 猫猫向前冲(拓扑排序)
- 班长竞选(Kosaraju模拟)
二、逐个击破
1.区间选点II(差分约束系统)
题目描述
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
- Input
输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。
- Output
输出一个整数表示最少选取的点的个数
题目分析
- 差分约束系统
• 一种特殊的n 元一次不等式组,它包含n个变量以及m个约束条件。
• 每个约束条件是由两个其中的变量做差构成的,形如 x i − x j ≤ c k x_i-x_j\leq c_k xi−xj≤ck ,其中 c k c_k ck 是常数(可以是非负数,也可以是负数)。
• 我们要解决的问题是:求一组解 x 1 = a 1 x_1=a_1 x1=a1 , x 2 = a 2 x_2=a_2 x2=a2 , … , x n = a n x_n=a_n xn=an ,使得所有的约束条件得到满足,否则判断出无解。
注意到,如果
a
1
,
a
2
,
a
3
,
…
,
a
n
{a_1, a_2, a_3, … , a_n }
a1,a2,a3,…,an 是该差分约束系统的一组解,那么对
于任意的常数𝑑,显然
a
1
+
d
,
a
2
+
d
,
a
3
+
d
,
…
,
a
n
+
d
{a_1+d, a_2+d, a_3+d, … , a_n +d}
a1+d,a2+d,a3+d,…,an+d 也是该差分约束系统的一组解,因为这样做差后𝑑 刚好被消掉。
-
结论
求解差分约束系统,都可以转化为图论中单源最短路问题, 对于差分约束中的每一个不等式约束 x i − x j ≤ c k x_i-x_j\leq c_k xi−xj≤ck都可以移项变形为 x i ≤ c k + x j x_i\leq c_k+x_j xi≤ck+xj,如果令 c k = w ( i , j ) , d i s [ i ] = x i , d i s [ j ] = x j c_k=w(i,j),dis[i]=x_i,dis[j]=x_j ck=w(i,j),dis[i]=xi,dis[j]=xj,那么原式变为 d i s [ i ] ≤ d i s [ j ] + w ( i , j ) dis[i]\leq dis[j]+w(i,j) dis[i]≤dis[j]+w(i,j),与最短路问题中的松弛操作很像! -
注意
在求解最短路的时候,程序是按照等于号来计算的,此时跑最短路得到的是一组最大解(上界),如果想要求最小解(下界)则将≤变成≥然后跑最长路即可,原理都是一样的
下面具体分析这道题目如何构造不等式组,记 d i s [ i ] dis[i] dis[i]表示数轴上 [ 0 , i ] [0,i] [0,i]之间的选点个数,对于第i个区间 [ a i , b i ] [a_i,b_i] [ai,bi]需要满足 d i s [ b i ] − d i s [ a i − 1 ] ≥ c i dis[b_i]-dis[a_i-1]\geq c_i dis[bi]−dis[ai−1]≥ci,另外还需要保证 0 ≤ d i s [ i ] − d [ i − 1 ] ≤ 1 0\leq dis[i]-d[i-1]\leq 1 0≤dis[i]−d[i−1]≤1,即保证第i个点要么选要么不选,然后求解该差分约束系统的最小解,转化为大于等于好求单源最长路,最后的答案为 d i s [ m a x [ b i ] ] dis[max[b_i]] dis[max[bi]]
然后下面是题目的全部代码:
#include<iostream>
#include<queue>
#include<cstring>
#define inf 1e8
using namespace std;
const int N = 5*1e4+10;
const int M = (5*1e4+10)*3;
int w[N],vis[N],dis[N];
//vis-点在不在队列中
//dis[x]-距离
int head[N],tot,n;
struct edge
{
int to,nxt,w;
}e[M];
void add(int x,int y,int w)
{
e[tot].to = y;
e[tot].nxt = head[x];
e[tot].w = w;
head[x] = tot;
tot++;
}
void init(int n)
{
tot=0;
for(int i=0;i<n;i++)
{
head[i]=-1;
dis[i]=-inf;//一定注意跑最长路这里要赋值为负的
}
memset(vis,0,sizeof(vis));
}
queue<int> q;
void spfa(int s)
{
//队列中加入初始点
dis[s] = 0,vis[s] = 1;
q.push(s);
while(!q.empty())
{
int x = q.front(); q.pop();
//x出队列,vis[x] = 0
vis[x] = 0;
//松弛操作
for(int i=head[x];i!=-1;i = e[i].nxt)
{
int y = e[i].to;
if(dis[y]<dis[x]+e[i].w)
{//跑最长路问题
dis[y] = dis[x] + e[i].w;
if(!vis[y])
{
vis[y] = 1;
q.push(y);
}
}
}
}
}
int main()
{
int num,maxb=-1;
scanf("%d",&num);
init(N);
for(int i=0;i<num;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b+1,c);
if(b+1>maxb) maxb=b+1;
}
for(int i=0;i<=maxb;i++)
{
add(i,i+1,0);
add(i+1,i,-1);
}
spfa(0);
printf("%d",dis[maxb]);
return 0;
}
Tips:由于会出现 d i s [ i − 1 ] dis[i-1] dis[i−1],所以防止处理的数据会出现 i = 0 i=0 i=0的情况,所以整体向右移动一格单位进行存储数据!!(这也是附加数据的考点)
2.猫猫向前冲(拓扑排序)
题目描述
有一天,TT 在 B 站上观看猫猫的比赛。一共有 N 只猫猫,编号依次为1,2,3,…,N进行比赛。比赛结束后,Up 主会为所有的猫猫从前到后依次排名并发放爱吃的小鱼干。不幸的是,此时 TT 的电子设备遭到了宇宙射线的降智打击,一下子都连不上网了,自然也看不到最后的颁奖典礼。
不幸中的万幸,TT 的魔法猫将每场比赛的结果都记录了下来,现在他想编程序确定字典序最小的名次序列,请你帮帮他。
- Input
输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示猫猫的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即编号为 P1 的猫猫赢了编号为 P2 的猫猫。
- Output
给出一个符合要求的排名。输出时猫猫的编号之间有空格,最后一名后面没有空格!
其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。
题目分析
拓扑排序是有向无环图顶点的线性排序,拓扑排序最常用的算法为Kahn算法。在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,才能称为该图的一个拓扑排序:
- 序列中包含每一个顶点,且每个顶点只出现一次;
- 若A在序列中排在B的前面,则在图中不存在从B到A的路径。
原理:
- 将入度为0的点组成一个集合S
- 每次从S里面取出一个顶点u(可以随便取)放入L , 然后遍历顶点u的
所有边(u, v) , 并删除之,并判断如果该边的另一个顶点v,如果在移除
这一条边后入度为0 , 那么就将这个顶点放入集合S中。不断地重复取
出顶点然后重复这个过程。 - 最后当集合为空后,就检查图中是否存在任何边。如果有,那么这个图
一定有环路,否者返回L , L中顺序就是拓扑排序的结果
伪代码如下图所示:
现在具体分析这一道题目,其实本质就是一道单纯的拓扑排序问题,唯一有些不同的是由于答案可能有多组,在输出的时候要求按照字典序最小进行输出,自然我们不可能随即求出一组答案然后进行字典序调整,因为拓扑关系就无法保证了,所以要求我们输出即为所求,那么就应该从数据结构的角度取思考,采取的解决方式是将队列改为使用最小堆,这样就可以保证取队首的时候可以取队列中编号最小的点出队,然后我再描述图的信息的时候使用的是链式前向星。
注意:priority_queue默认的优先级队列的形式为最大堆,可以将压入的数改为负数就可以变为最小堆,别忘记取出后进行取反!
综上所述,该题全部解决代码如下:
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int N = 505;
const int M = 500*500;
//vis-点在不在队列中
//dis[x]-距离
int head[N],in_deg[N],tot;
struct edge
{
int to,nxt;
}e[M];
void add(int x,int y)
{
e[tot].to = y;
e[tot].nxt = head[x];
head[x] = tot;
tot++;
}
void init()
{
for(int i=0;i<N;i++)
{
head[i]=-1;
in_deg[i]=0;
tot=0;
}
}
int main()
{
int N,M;
while(~scanf("%d%d",&N,&M))
{
init();//初始化
while(M--)
{
int P1,P2;
scanf("%d%d",&P1,&P2);
add(P1,P2);
in_deg[P2]++;
}
priority_queue<int> q;
for(int i=1;i<=N;i++)
if(in_deg[i]==0) q.push(-i);//放入负数变为最小堆
vector<int> ans;
while(!q.empty())
{
int u = -q.top();
q.pop();
ans.push_back(u);
for(int i=head[u];i!=-1;i=e[i].nxt)
if(--in_deg[e[i].to]==0) q.push(-e[i].to);
}
for(int i=0;i<N;i++)
{
printf(i==N-1?"%d\n":"%d ",ans[i]);
}
}
return 0;
}
3.班长竞选(Kosaraju模拟)
题目描述
大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?
- Input
本题有多组数据。第一行 T 表示数据组数。每组数据开始有两个整数 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下来有 M 行包含两个整数 A 和 B(A != B) 表示 A 认为 B 合适。。
- Output
对于每组数据,第一行输出 “Case x: ”,x 表示数据的编号,从1开始,紧跟着是最高的票数。 接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!
题目分析
强连通分量(SCC)
强连通分量定义为极大的强连通子图,其中极大的意思是点尽可能的多;如果两个点
A
A
A和
B
B
B是强连通的,说明存在A到B的路径也存在B到A的路径。如下红色方框内为一个SCC,蓝色方向表示SCC之间的连接关系。
接下来就是在已知图的基础上如何找出其中的SCC,这就需要用到DFS序的概念,DFS前序表示第一次达到点x 的次序,用
d
[
x
]
d[x]
d[x] 表示;DFS后序表示x 点遍历完成的次序,即回溯时间,用
f
[
x
]
f[x]
f[x] 表示;逆后序是指后序序列的逆序,有了DFS序的概念,下面介绍找出SCC的算法——Kosaraju算法
Kosaraju算法
- 第一遍dfs 确定原图的逆后序序列
- 第二遍dfs 在反图中按照逆后序序列进行遍历
反图即将原图中的有向边反向
每次由起点遍历到的点即构成一个SCC
那么对于这道具体的题目来说,我们发现由于意见存在传递性,所以我们需要找到的就是这个图所对应的强连通分量,然而我们不仅需要知道一个SCC中有几个点,还需要求出SCC之间的传递关系,那么如果我们对于每一个小的结点都求一遍dfs进行求和这样复杂度太高了,所以我们考虑缩点,将一个SCC中的所有点认为是一个“大点”,如果以我们上面列出的图来说明缩点即为如下情况:
缩点后,不难发现对于属于第i 个SCC 的点来说,答案分为两部分,令SCC[i] 表示第i 个SCC 中点的个数,当前SCC 中的点,
a
n
s
=
S
C
C
[
i
]
−
1
ans=SCC[i]-1
ans=SCC[i]−1,再加上其他SCC中的点
S
U
M
(
S
C
C
[
j
]
)
SUM(SCC[j])
SUM(SCC[j]),其中j可以到达i,但是不难发现稍加思考,可以发现最后答案一定出现在出度为0 的SCC中,可以用反证法证明所,以这道题目具体的代码如下:
#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
using namespace std;
const int MAXN = 5005;
const int MAXM = 3*1e4+5;
int head1[MAXN],head2[MAXN],head3[MAXN],tot1,tot2,tot3;
int c[MAXN],dfn[MAXN],vis[MAXN],dcnt,scnt;
//dcnt--dfs序计数,scnt--scc计数 ,c[i]--i号点所在的SCC编号
int scc[MAXN],in_deg[MAXN];//scc[i]--表示第i个scc中点的个数
int ans[MAXN];//最后输出的答案
struct edge
{
int to,nxt;
}e1[MAXM],e2[MAXM],e3[MAXM];//e1为原图,e2为反图,e3为scc点的反图
void add1(int x,int y)
{
e1[tot1].to = y;
e1[tot1].nxt = head1[x];
head1[x] = tot1;
tot1++;
}
void add2(int x,int y)
{
e2[tot2].to = y;
e2[tot2].nxt = head2[x];
head2[x] = tot2;
tot2++;
}
void add3(int x,int y)
{
e3[tot3].to = y;
e3[tot3].nxt = head3[x];
head3[x] = tot3;
tot3++;
}
void init()
{
tot1=tot2=tot3=0,dcnt=0,scnt=0;
for(int i=0;i<MAXN;i++)
{
head1[i]=-1;
head2[i]=-1;
head3[i]=-1;
c[i]=0;
dfn[i]=0;
vis[i]=0;
scc[i]=0;
in_deg[i]=0;
ans[i]=0;
}
for(int i=0;i<MAXM;i++)
{
e1[i].nxt=0;
e1[i].to=0;
e2[i].nxt=0;
e2[i].to=0;
e3[i].nxt=0;
e3[i].to=0;
}
}
void dfs(int x,int m)
{
vis[x]=1;
for(int i=head3[x];i!=-1;i=e3[i].nxt)
{
if(!vis[e3[i].to])
{
ans[m]+=scc[e3[i].to];
dfs(e3[i].to,m);
}
}
}
void dfs1(int x)
{
vis[x]=1;
for(int i=head1[x];i!=-1;i=e1[i].nxt) if(!vis[e1[i].to]) dfs1(e1[i].to);
dfn[++dcnt] = x;
}
void dfs2(int x)
{
c[x]=scnt;
for(int i=head2[x];i!=-1;i=e2[i].nxt) if(!c[e2[i].to]) dfs2(e2[i].to);
scc[scnt]++;
}
void kosaraju(int n)
{
//初始化
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
for(int i=0;i<n;i++)
if(!vis[i])dfs1(i);
for(int i=n;i>=1;i--)
if(!c[dfn[i]]) ++scnt,dfs2(dfn[i]);
}
int main()
{
int T;
scanf("%d",&T);
int cc=1;
while(T--)
{
init();
int N,M;
scanf("%d%d",&N,&M);
while(M--)
{
int A,B;
scanf("%d%d",&A,&B);
add1(A,B);
add2(B,A);//构建反图
}
vector<int>ps;
kosaraju(N);
//缩点
for(int i=0;i<N;i++)
for(int j=head1[i];j!=-1;j=e1[j].nxt)
{//遍历所有的边
int y=e1[j].to;
if(c[i]!=c[y])
{
add3(c[y],c[i]);
in_deg[c[i]]++;
}
}
int max_ans=-1;
for(int i=1;i<=scnt;i++)
{
if(in_deg[i]==0)
{
ans[i]+=scc[i]-1;//先加上自己scc中的认同个数
memset(vis,0,sizeof(vis));
dfs(i,i);
}
if(max_ans<ans[i])max_ans=ans[i];
}
printf("Case %d: %d\n",cc,max_ans);
cc++;
int out_cnt=0;
for(int i=0;i<N;i++)
if(ans[c[i]]==max_ans)
{
if(out_cnt==0)
{
printf("%d",i);
out_cnt++;
}
else printf(" %d",i);
}
printf("\n");
}
return 0;
}