前言
如果我们把每个员工看成图上的一个节点,员工
x
x
x 喜欢员工
y
y
y,就在从
x
x
x 对应的节点到
y
y
y 对应的节点连一条边,那么形成的图则会由若干棵 基环内向树
组成。所谓 基环内向树
就是形如下图所示的结构:
原因如下:
- 从任意一个节点
x
x
x 开始在图上进行
游走
,由于每个员工只有一位喜欢的员工,因此每个节点在图上只有一条出边,即游走
的过程是唯一的。由于图上有 n n n 个节点,因此在 n + 1 n+1 n+1 步以内,一定会走到一个重复的节点,那么在第一次经过该节点之后,到第二次经过该节点之前的所有节点及该节点本身就组成了一个环,如上图蓝色节点所示。 - 对于不在环上的节点,我们已经说明了从它们开始
游走
也一定会进入到环中。到达环上的节点之前,它们不会重复经过节点(否则就有两个环了,我们可以证明一个连通分量中不可能有两个环:因为每个节点只有一条出边,因此如果有两个环并且它们连通,那么必然某个环上有一个点有两条出边,一条出边指向同一个环上的节点,另一条出边可以使它到达另一个环,这就产生了矛盾),那么它们就形成了类似树的结构,如上图绿色节点所示。
方法一:动态规划+拓扑排序
既然我们知道了图由若干棵 基环内向树
组成,那么我们就可以想一想,每一棵 基环内向树
的哪一部分可以被安排参加会议?
我们首先讨论特殊的情况,即一个单独的环(或若干个环),并且所有环的大小都 ≥ 3 \ge3 ≥3。可以发现,我们按照环上的顺序给对应的员工安排座位是满足要求的,因为对于每一个环上的员工,它喜欢的员工就在它旁边。并且,我们必须安排环上的所有员工,因为如果有缺失,那么喜欢那位缺失了的员工就无法满足要求了。
但如果我们已经安排了某一个环上的所有员工,剩余的环就没办法安排了。这是因为已经安排的那个环是没办法被断开的:断开的本质就是相邻位置员工的缺失。因此,我们可以得出一个重要的结论:如果我们想安排大小 ≥ 3 \ge3 ≥3 的环,我们最多只能安排一个,并且环需要是完整的。
那么如果是环大小
≥
3
\ge3
≥3 的 基环内向树
呢?如果我们安排了不在环上的节点,那么从该节点开始,我们需要不断安排当前节点喜欢的员工,这实际上就是 游走
的过程,而当我们游走到环上最后一个未经过的节点时,该节点的下一个节点(即喜欢的员工)已经被安排过,所以最后一个未经过的节点就无法被安排,不满足要求。因此,我们不能安排任何不在环上的节点,只能安排在环上的节点,就得出了另一个结论:所有环
≥
3
\ge3
≥3的 基环内向树
与一个大小相同(指环的部分)的环是等价的。
那么最后我们只需要考虑大小
=
2
= 2
=2 的环或者 基环内向树
了。这里的特殊之处在于,大小
=
2
=2
=2 的环可以安排多个 :因为环上的两个点是 互相喜欢 的,因此只需要它们相邻即可。而对于环大小
=
2
=2
=2 的 基环内向树
,如果我们安排了不在环上的节点,那么游走完环上两个节点之后,同样是满足要求的,并且我们甚至可以继续延伸(反向 游走
),到另一个不在环上的节点为止。如图所示,包含
x
x
x 的节点就是可以安排参加会议的节点。
并且同样地,对于每一棵环大小
=
2
=2
=2 的 基环内向树
,我们都可以取出这样一条 双向游走
路径进行安排,它们之间不会影响。综上所述,原问题的答案即为下面二者中的最大值:
- 最大环的大小
- 所有环大小
=
2
=2
=2 的
基环内向树
上的最长的双向游走
路径之和
为了求解 基环内向树
上最长的 双向游走
路径,我们可以使用 拓扑排序+动态规划 的方法。记
f
[
i
]
f[i]
f[i] 表示到节点
i
i
i 为止的最长 游走
路径经过的节点个数,那么状态方程即为:
f
[
i
]
=
max
j
→
i
{
f
[
j
]
}
+
1
f[i]=\max_{j\rightarrow i}\{f[j]\} + 1
f[i]=j→imax{f[j]}+1即我们考虑节点
i
i
i 的上一个节点
j
j
j,在图中必须有从
j
j
j 到
i
i
i 的一条有向边,这样我们就可以从
j
j
j 转移到
i
i
i。如果不存在满足要求的
j
j
j(例如 基环内向树
退化成一个大小
=
2
=2
=2 的环),那么
f
[
i
]
=
1
f[i]=1
f[i]=1。状态转移和拓扑排序可以同时进行。
在拓扑排序完成后,剩余没有被弹出过队列的节点就是环上的节点。我们可以找出每一个环。如果环的大小
≥
3
\ge 3
≥3,我们就用其来更新最大的环的大小;如果环的大小
=
2
=2
=2,设环上的两个节点为
x
x
x 和
y
y
y,那么该 基环内向树
上最长的 双向游走
的路径长度就是
f
[
x
]
+
f
[
y
]
f[x]+f[y]
f[x]+f[y]。
class Solution {
public int maximumInvitations(int[] favorite) {
int n = favorite.length;
int[] indeg = new int[n]; //统计入度,便于进行拓扑排序
for(int i = 0; i < n; i++){
++indeg[favorite[i]];
}
boolean[] used = new boolean[n];
int[] f = new int[n]; //f[i]表示到节点i为止的最长路径经过的节点个数
Arrays.fill(f, 1);
Queue<Integer> queue = new ArrayDeque<Integer>();
for(int i = 0; i < n; i++){ //寻找入度为0的节点,并入队
if(indeg[i] == 0){
queue.offer(i);
}
}
while(!queue.isEmpty()){ //拓扑排序+动态规划:求到节点i为止的最长路径经过的节点个数
int u = queue.poll();
used[u] = true;
int v = favorite[u];
f[v] = Math.max(f[v], f[u] + 1);//状态转移
--indeg[v]; //v节点入度减1
if(indeg[v] == 0){ //若入度为0,则入队
queue.offer(v);
}
}
int ring = 0; //表示最大环的大小
int total = 0; //表示所有环大小为2的基环内向树树上的最长的双向游走路径之和
for(int i = 0; i < n; ++i){
if(!used[i]){
int j = favorite[i];
if(favorite[j] == i){ //说明环的大小为2
total += f[i] + f[j];
used[i] = used[j] = true;
}else{ //否则环的大小至少为3,我们需要找出环
int u = i, cnt = 0;
while(true){ //求环的大小
++cnt;
u = favorite[u];
used[u] = true;
if(u == i){
break;
}
}
ring = Math.max(ring, cnt);
}
}
}
return Math.max(ring, total);
}
}