算法好题积累——CF1764C 图+贪心

1764C. Doremy’s City Construction

题目链接:https://codeforces.com/problemset/problem/1764/C
关键词:图;贪心算法;python
CF难度:1400
前置知识:图的基本概念、列表的排序与顺序查找

一、题目

Doremy’s new city is under construction! The city can be regarded as a simple undirected graph with 𝑛 vertices. The 𝑖-th vertex has altitude 𝑎𝑖. Now Doremy is deciding which pairs of vertices should be connected with edges.
Due to economic reasons, there should be no self-loops or multiple edges in the graph.
Due to safety reasons, there should not be pairwise distinct vertices 𝑢, 𝑣, and 𝑤 such that 𝑎𝑢≤𝑎𝑣≤𝑎𝑤 and the edges (𝑢,𝑣) and (𝑣,𝑤) exist.
Under these constraints, Doremy would like to know the maximum possible number of edges in the graph. Can you help her?
Note that the constructed graph is allowed to be disconnected.

Input
The input consists of multiple test cases. The first line contains a single integer 𝑡 (1≤𝑡≤104) — the number of test cases. The description of the test cases follows.
The first line of each test case contains a single integer 𝑛 (2≤𝑛≤2⋅105
) — the number of vertices.
The second line of each test case contains 𝑛 integers 𝑎1,𝑎2,…,𝑎𝑛
(1≤𝑎𝑖≤106) — the altitudes of each vertex.
It is guaranteed that the sum of 𝑛 over all test cases does not exceed 2 ⋅ 1 0 5 2⋅10^5 2105.

Output
For each test case, output the maximum possible number of edges in the graph.

二、思路

注:以下分析只是个人在思考本问题时的思考过程,若有麻烦和不完备之处请多多见谅~也非常欢迎大家在评论区写出更好的理解!

这是一道图论问题,题目的任务是构造一个n节点的简单无向图,使得图在满足以下三个条件的同时,具有最多的边数。1、图的节点数量为 𝑛,节点 𝑖 的海拔为 𝑎𝑖。2、图中不存在自环和多重边。3、不存在三个不同的节点 𝑢, 𝑣, 𝑤 满足 𝑎𝑢≤𝑎𝑣≤𝑎𝑤 并且边 (𝑢,𝑣) 和 (𝑣,𝑤) 都存在。解决这个问题可以帮助Doremy在综合考虑经济因素和安全因素的前提下,城市建造所需要的最大预算,有很强的实际意义。

前两个条件给出了图的基本结构形式:有n个节点,节点之间由线段相连,不存在A→A的自环,A与B之间最多只能由一条线段相连。解题的重点在于对第三个条件的分析。这是一个对三个点的集合存在性的描述,我们要将其转化为对单个点的描述:即对每一个点,他所连接的所有点的海拔,要么全严格大于他的海拔,要么全严格小于他的海拔。反之,我们就能找到这样的𝑎𝑣,满足 𝑎𝑢≤𝑎𝑣≤𝑎𝑤 并且边 (𝑢,𝑣) 和 (𝑣,𝑤) 都存在。

当然,上述转化的前提是,图中的每一个点所连接的点的数量大于等于2。而当某一些点,他们中的每一个点都只连接一个其他点时,就将不存在三个不同的节点 𝑢, 𝑣, 𝑤,并且边 (𝑢,𝑣) 和 (𝑣,𝑤) 都存在,也就自然不与条件3相违背。当给定的n个节点海拔全相等时,任意三个节点的相互连通都将违背条件3,因此,每个点都只能连接其他一个点。

我们在最后讨论这种策略。先不采取这种策略,则要采取策略“对每一个点,他所连接的所有点的海拔,要么全严格大于他的海拔,要么全严格小于他的海拔”,那么就可以将n个点(n>=2)自然分为两类:连接的点的海拔全都比他大的点(A集合),和连接的点的海拔全都比他小的点(B集合)。

可以推出以下策略:

1、在两个集合内部的点之间不能有连线。否则这两个点互相都比对方大或小,显然矛盾。
因此,最终的连线情况即为两个集合的点之间相互连线。

2、海拔相同的点在同一个集合中时,连线尽可能的多
设这两个集合为A和B,分别有i个元素和(n-i)个元素。现有k1+k2个海拔相等的点,A中分布有k1个,B中分布有k2个,他们的ai都相等。假设除了这k1+k2个点外,其他的点的ai互不相等。现考虑这样两种操作,把A中的这k1个点转移到B中,和把B中的这k2个点转移到A中。
不操作时和做两种操作时,可连线条数分别为
i ∗ ( n − i ) − k 1 ∗ k 2 i*(n-i)-k_1*k_2 i(ni)k1k2 ( i − k 1 ) ∗ ( n − i + k 1 ) (i-k_1)*(n-i+k_1) (ik1)(ni+k1) ( i + k 2 ) ∗ ( n − i − k 2 ) (i+k_2)*(n-i-k_2) (i+k2)(nik2)

令X=i-k1,Y=n-i-k2。当X>Y时,(2)>(1)>(3);当X<Y时,(3)>(1)>(2);当X=Y时,(3)=(1)=(2)。因此,对于任意的满足假设的集合A和B,总能通过上述两种操作中的一种,让总边数上升。合理外推,即可得到结论:海拔相同的点在同一个集合中时,连线尽可能的多。

现在,将海拔相同的点归位一组,根据策略2,这一组的点要么全与海拔比他大的点相连(属于集合A),要么全与海拔比他小的点相连(属于集合B)。

对每一组这样的点,假定这样的贪心策略:若海拔比他大的点的数量大于或等于海拔比他小的点的数量,则将这组点归为集合A,反之则归为集合B。遍历所有的组,并把它们按照上述方法归类。

这种贪心策略的结果与遍历各个组的先后顺序无关,在下面的代码中,als.sort()把输入的乱序的数字分成组for i in range(1,N)if als[i]!=als[i-1]用于从前到后扫描以读取各个组的开头位置。

读取到一个组的开头位置i后,需要对海拔比他大的点和海拔比他小的点计数,以确定这组点归为集合A还是集合B。这就是als.sort()的另一个作用:位置i左侧的所有点的海拔都严格小于这个点的海拔,因此海拔比他小的点的数量为i;位置i右侧的所有点海拔都大于或等于这个点的海拔,借助forward_count()函数去除掉海拔相等的点,剩下的N-i-forward_count(i)即为海拔比这个点大的点的数量。若找到第一个组满足i>N-i-forward_count(i),则应归为集合B。这个组之前的组则归为集合A。

对于这个组之后的组,其海拔比这个组更大,海拔比他大的点数量更少,海拔比他小的点数量更多,则更应该归为集合B。因此找到第一个归为集合B的组后就可以结束循环,节约时间。

根据贪心策略,我们设计了一个完全等价的读取数据的策略,而这个策略的结果显示集合A中点的海拔全部小于集合B中点的海拔。这也就说明集合A中的每一个点,都能与集合B中的每一个点相连。所以,最大边数即为i*(n-i)

而当给定的n个节点海拔全相等时,找不到i使als[i]!=als[i-1],任意三个节点的相互连通都将违背条件3,每个点都只能连接其他一个点,最终的结果即为N//2(N为奇数时将有一个点不连接其他点)。

代码

def forward_count(index):
    global als
    forwardcount=0
    for forwardindex in range(index,N):
        if als[forwardindex]==als[index]:
            forwardcount+=1
        else:
            break
    return (forwardcount)#含自身
n=int(input())
result=[]
for _ in range(n):
    N=int(input())
    als=[int(x) for x in input().split()]
    als.sort()
    flag=False
    for i in range(1,N):
        if als[i]!=als[i-1]:#找到了一个组的开头位置
            if i>N-i-forward_count(i):
            #这个点左边所有点的数量>这个组右边组的点的数量
                flag=True
                break#找到了第一个向左连线的位置i。
    if flag:
        roads=i*(N-i)
        result.append(roads)
    else:#所有数都相等时
        result.append(N//2)

for res in result:
    print(res)
#264ms 25700KB

那么对于不全相等的ai,“每一个点都只连接一个其他点”的策略能否使边的数量最多呢?

n为偶数时,采取该策略的最大边数是n/2。n为奇数时,采取这种策略的最大边数是n//2,即(n-1)/2。若采取上述贪心策略,其最大边数是x(n-x)。当ai不全相等时,一定能将所有ai分成两个集合A和B,也就是说x>=1。所以x(n-x)>=n-1。n>2时,n-1>n/2>(n-1)/2。n=2时选取何种策略都只有一条边。故对不全相等的ai,应采取上述贪心策略。

同理,对于不全相等的ai,一部分点采取“每一个点都只连接一个其他点”策略,一部分点采取上述贪心策略的最大边数,则小于两部分点各自采取贪心策略的最大边数。而对于两部分各自采取贪心策略的情况,如下图所示,A1中所有点海拔小于A2中的,B1中的所有点海拔小于B2中的。

在这里插入图片描述

将所有点和起来分割成(A1+B1)和(A2+B2)两部分。若B2中的所有点<=A1中的所有点,则B1中的

所有点<B2中的所有点<=A1中的所有点<A2中的所有点,若A2中的所有点<=B1中的所有点,则A1中的

所有点<A2中的所有点<=B1中的所有点<B2中的所有点。即在新的分隔方式中,若A1和B2之间不能连线,则B1和A2之间可以连线,若B1和A2之间不能连线,则A1和B2之间可以连线。再加上A1和A2之间、B1和B2之间的连线,可以说明:两部分点各自采取贪心策略的最大边数,小于所有点采取贪心策略的最大边数。

最后,试图说明这种贪心策略的正确性。基于推论1和2,把n个数按照海拔分为k个组并排序(就像上面解释的那样),所有可能的组间连线数量是1+2+···+(n-1),接下来要剔除掉一些组间连线,使每一组要么只有向左连的线,要么只有向右连的线。从左到右对每一组进行剔除,上述贪心策略在这个过程中的体现就是:若这一组当前向左连的线多于向右连的线,那么就剔除向右的线,反之则剔除向左的线。

实际上,上述贪心策略相当于不进行剔除操作的一种“预测”,而预测的结果和这种贪心剔除原则结果相同。因为刚开始的几组剔除向左的,保留向右的,而剔除当前组左边的线对后一组没有影响,所以在从左到右剔除的顺序之下,后一组向左还是向右连与前一组向左还是向右连无关。

以下图为例,结果为右右右左左,因为前三组都只能向右连,所以前三组之间没有连线,同理后两组之间也没有连线。总边数就是3x2=6。.

在这里插入图片描述

如果不用贪心原理,也可以解出这道题目。根据1、2两条策略,进行同样的分组、排序。在剔除过程中,每一组可以任意选择保留左边或者右边。根据上面证过的结论,当“左”和“右”的数量一定时,右左右左右、右右左左右等边数统统小于“右右右左左”。当向左的点的数量和向右的点的数量加和一定(为n)时,当他们的数量最接近时,乘积有最大值(二次函数单调性)。如下代码1所示,N//2的位置一定落在某个同海拔的组内,找到距离N//2最近的组间交界,即找到以该组左右边界为A、B集合交界时,乘积的最大值。或如代码2,直接遍历所有可能的i*(n-i),找到其中的最大值。

#代码1 187ms 26400KB
t=int(input())
while t!=0:
    t-=1
    n=int(input())
    a=list(map(int,input().split()))
    a=sorted(a)
    b=sorted(a,reverse=True)
    x=a.index(a[n//2])
    y=b.index(a[n//2])
    print( max( x*(n-x) , y*(n-y) , n//2 ) )
#代码2 249ms 26400KB
r=[]
for i in range(int(input())):
    n = int(input())
    L = sorted(list(map(int, input().split())))
    ans = n//2
    for i in range(1, len(L)):
        if L[i] != L[i-1]:
            ans = max(ans, i*(n-i))
    r.append(ans)
for re in r:
    print(re)

总结:通过这道题,充分认识到了贪心题目的复杂和困难,若想到了贪心策略就不难做对,否则就十分困难。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值