Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Comments on What's the complexity of this algorithm for solving the maximum clique NP-Complete problem?

Post

What's the complexity of this algorithm for solving the maximum clique NP-Complete problem?

+3
−0

I believe I have discovered a quasi-polynomial algorithm for solving an NP-Complete problem: the maximum clique problem.

Of course, I doubt the correctness of my discovery, because as we know, that's known to be quite difficult and unlikely.

I'll present here the essential algorithm (without optimizations that are unnecessary for reaching QP complexity). I'm interested in finding whether my evaluation of the worst-case complexity is correct.

The algorithm below is recursive:

  1. Take as input the minimum size that a solution must have to be a better clique than those known so far (initially, use 1). Let's call it Z.

  2. Take as input a problem that has nodes, some of which are connected through edges. Let's call this problem P.

  3. Create an empty solution list. Let's call it S.

  • 2bis) [Edit: I forgot to include this step]

    Remove any nodes that have less than Z-1 edges. Repeat this operation as necessary.

  1. Create a copy of P. Let's call it A. It holds the only candidates that can be selected in step 4. At the moment, all nodes of P are also contained in A.

loop:

  1. Find the best candidate node. Let's call it X.

    The candidate node must exist in A. From those that exist in A, select the one with less edges in P, even if this might seem counter-intuitive.

  2. If there are no candidates, print S (it might be empty), and terminate (if we were recursing, continue at step 9 of the previous level of recursion; otherwise, we've finished).

  3. Create a temporary solution list containing exactly X. Let's call it ST.

  4. Create a smaller problem Q, which is a copy of P, but removing any nodes not connected with X, and also removing X.

  5. Solve (recursively, goto step 0) the problem Q. Pass MAX(Z-1, 1) as the input for step 0, and Q as the input for step 1.

    Because X is the least-connected node, Q is the smallest problem we could try to solve. Considering that p (which is 0<=p<=1) is the frequency of edges in the problem P, and n is the number of nodes in the problem P, Q will have no more than n*p nodes.

  6. Append the output of the previous step to the list ST.

  7. If the size of the list ST is greater or equal than Z, replace S with the contents of ST, and update Z to be the size of ST plus 1.

  8. Remove ST.

  9. Create a copy of P. Let's call it AT.

  10. Remove from AT all nodes that are connected to X, and remove also X from A.

    The reason is that we've found the best solution that contains X, and thus if we want a better solution, it cannot contain X, and it must contain at least one node not compatible with X. Actually, because we want a better solution, we need at least two nodes not connected to X. Thus,

  11. For each node that exists in A and/or in AT, if in any of those graphs the node is not connected to any other nodes, remove such node from all of P, A, and AT. (Edited, as I had forgotten a detail.)

    These can only start a solution as good as S, but not better.

  12. Remove X from P.

  • 15bis) [Edit: I forgot to include this step]

    Remove any nodes that have less than Z-1 edges in P from both P, A, and AT.

  1. If AT contains less nodes than A, replace A with the contents of AT.

    This is an optimization, so that we use the smallest A (alternatives list), which reduces the number of iterations in the loop. This reduces the base of the exponential function, but is not necessary for the algorithm being QP.

  2. Remove AT.

  3. goto step 4 (loop);

Signed-off-by: Alejandro Colomar Andrés <[email protected]>

(Note: This was all human generated; I haven't used any LLM or AI at all for developing this algorithm; just my brain.)

This is the first time I publish this algorithm. I will soon publish my own implementation, which contains a few important optimizations. (If you try implementing the above, you'll find it's very very slow, and some optimizations not mentioned here are necessary to make it be actually fast.)


In steps 4 and 8 are the key to the super-polynomial complexity.

The number of times we repeat step 4 across all recursion levels is essential to the complexity of the algorithm. (Then, that's multiplied by the complexity of each iteration, but that is polynomial, so let's ignore that part.)

Some interesting facts:

  • The depth of recursion will not grow past the size of the maximum clique.
  • The size of the problems P will diminish until it's 0 at the depth of recursion of the size of the maximum clique.
  • When getting close to that recursion level, p will grow to 1.

The amount of iterations of step 4 in a given recursion level is bound by the number of nodes at that level.

The first levels of recursion will reduce the size faster than n*p^d where d is the depth level, given we choose candidates with number of edges below average. We might worry about the deeply recursive levels, where we got rid of the nodes with few edges, and only highly connected nodes remain.

Let's start by assuming that p is homogeneous across all nodes, so that all nodes have n*p edges, all the time. It's obvious that the size of the problem at each level of recursion will be p times the size of the previous level of recursion. That way, we'll reach size 1 (and thus terminate recursing) when n*p^d == 1, where d is the depth of recursion. We can calculate d = log(n)/log(1/p).

Now, if we drop the assumption that p is homogeneous, we have to calculate the size as n*p1*p2*p3*p4..., where p1,p2,p3... are the pi values of the best candidate X at each level of recursion. Because on average they must be more or less equal to p, some will be smaller, and others will be higher. We also start by the lowest values, and only reach the high values as needed, so on average we remain below p. Now, similar to how the geometric mean is never larger than the arithmetic mean, it seems obvious that the multiplication of non-homogeneous values will always result in a value smaller than the multiplication of homogeneous values.

By the end of the run of this algorithm, we'll be trying the remaining (highly connected) nodes at low recursion depths, which might also seem problematic. However, at that point, the number of edges of each node has dropped below half of the original, and thus, p must again be below average for those nodes.

As an exception to the above paragraph, in problems with disjoint subgraphs, with low p, the initial removal of nodes might not affect the p of the remaining last nodes. However, each of those disjoint subgraphs can be considered a smaller problem with a higher p, and the same applies to each smaller problem. Near the end, we'll face a problem of small n and high p, and the rules above will apply.

Thus, I believe I've proved that regardless of the distribution of p, the complexity of the exponent is bound by log(n)/log(1/p).

If the exponent is bound by a logarithm, and of course, the number of iterations of the loop in a given recursive call is bound by n, we can find the upper bound for the number of total iterations of step 4 by n^(log(n)/log(1/p)).

I've actually implemented this algorithm in a combination of shell scripts and C programs, and the number of iterations of each level of recursion, and the maximum depth reached, both seem to agree with these theoretical bounds. Actually, while they never surpass those bounds, they seem to be even better than those theoretical bounds. Also, I've run the implementation with several of the benchmarks I found at https://iridia.ulb.ac.be/~fmascia/maximum_clique/DIMACS-benchmark#detC125.9, and it agrees with the size of the maximum cliques, which makes me confident of the correctness of the algorithm (unrelated to its complexity).

Is all of this correct, or did I make any mistakes in my evaluation of the complexity?

Here are a few runs that seem to agree with the theory here:

N = 100, p = 0.9
$ time npc-rand 100 0.9 0 | npc-solve 100 | wc -l
Size: 100
Frequency: 0.900000
Seed: 0
     13 0
    151 1
   1369 2
   8148 3
  18268 4
   8259 5
    464 6
     11 7
      9 8
      9 9
      9 10
      9 11
      9 12
      9 13
      9 14
      9 15
      9 16
      9 17
      9 18
      9 19
      9 20
      8 21
      7 22
      6 23
      5 24
      4 25
      3 26
      2 27
      2 28
      1 29
30

real	8m42.176s
user	6m55.197s
sys	1m52.039s
N = 60, p = 0.9
$ time npc-rand 60 0.9 0 | npc-solve 60 | wc -l
Size: 60
Frequency: 0.900000
Seed: 0
      9 0
     39 1
     48 2
     11 3
      2 4
      2 5
      2 6
      2 7
      2 8
      2 9
      2 10
      2 11
      2 12
      2 13
      2 14
      2 15
      2 16
      2 17
      2 18
      2 19
      2 20
      2 21
      1 22
      1 23
24

real	0m1.142s
user	0m1.483s
sys	0m0.288s
History

4 comment threads

Two questions (21 comments)
Implementation source code (4 comments)
General comment (8 comments)
Translation to SAT (2 comments)
Two questions
clemens‭ wrote 5 months ago

I thought about the problem a little more and two questions occurred to me:

(1) What happens for $p$ very close to 1? E.g. this is the case when we use SAT to simulate a nondeterministic Boolean circuit. (2) Suppose we have shown that there exists an $n$-clique that contains $P$. But suppose there is no $n+1$-clique that contains $P$ but there is an $n+1$-clique that doesn't contain $P$. Then does this algorithm have to do an exhaustive search (with some branches pruned, of course) of the nodes connecting to $P$ in order to ascertain that there are no $n+1$-cliques containing $P$? If that is the case then the algorithm will have to be exponential (although I don't have a rigorous proof at the moment).

A further thought: most of the resources I see online have to do with SAT problems. It looks as though your clique algorithm has a very simple translation to SAT that could probably be found with a literature search, but I'm not quite sure about the exact details.

alx‭ wrote 5 months ago

(1)

For p very close to 1, most levels are only tried once. That is, the base of the exponential function becomes 1, and thus it's not really exponential, but rather polynomial. Here's a simple example:

For example, consider this graph:

11110111
11111011
11111101
11111110
01111111
10111111
11011111
11101111

Regardless of the node X you pick in step 4, it only has one unconnected node, so the graph AT in step 13 will have exactly one node. Because unconnected nodes in AT are immediately removed in step 14 we get an empty graph AT, which means there's no iteration.

Here's proof that the algorithm will only need one iteration per depth level:

alx@devuan:~/tmp$ cat high_p.txt 
11110111
11111011
11111101
11111110
01111111
10111111
11011111
11101111
alx@devuan:~/tmp$ cat high_p.txt | npc-solve 8 | wc -l
      1 0
      1 1
      1 2
      1 3
4

Then you would wonder what if there are two missing edges per node. That would seem to require iterating at least once...

alx‭ wrote 5 months ago · edited 5 months ago

(1, continuation)

Let's now consider a graph where the missing edges come in pairs, so that AT will have at least some nodes:

11110011
11110011
11111100
11111100
00111111
00111111
11001111
11001111

A problem with this graph is that it's so regular (symmetric) that once you've found the solution that contains one node, the alternatives will be discarded due to not having more edges than the known solution.

For example, let's pick the first node (0th, in my implementation, which has a 0-based index) as the candidate X. The first solution we find is obviously of size 4: 0,1,2,3.

$ cat not_so_high_p.txt | npc-subset 8 0 1 2 3
11110000
11110000
11110000
11110000
00000000
00000000
00000000
00000000

Then, we go into the search of better alternatives. In the AT graph, we have nodes 4,5 remaining. And in the P graph we have:

$ cat not_so_high_p.txt | npc-clear 0
00000000
01110011
01111100
01111100
00111111
00111111
01001111
01001111

...

alx‭ wrote 5 months ago · edited 5 months ago

(1, continuation)

Here, the algorithm I showed would try at least one of those, and immediately realize it doesn't lead anywhere, so the second recursion would only reach one level of depth. In my actual implementation, I have an optimization that can see that even before recursing.

I'll show here that optimization:

Consider we would pick node 4 as the best candidate. It is only unconnected to one existing node: 1. Because of that, we know that node 1 wouldn't be able to produce a solution better than node 4. Thus, we can remove node 1 from the graph above. And once we remove node 1, all nodes collapse, by not being connected enough. Thus, there's an optimization to not even recurse at all.

And as expected, the program only iterates once:

$ cat not_so_high_p.txt | npc-solve 8 | wc -l
      1 0
      1 1
      1 2
      1 3
4
alx‭ wrote 5 months ago · edited 5 months ago

(1, continuation)

Which leads us to think: at what point does it start to iterate? Is there a gap between the non-iterative solutions (very very high p), and the quasi-polynomial solutions (high p)? Is there a only-very-high p that would be exponential?

The key of the recursion level seems to be the difference between the smallest possible solution (the one we'd get in the first try), and the maximum clique. I think that's the exponent of the function.

So, in the first try, exactly at the point in step 5 where we first terminate and print S, that's the base solution. If the difference between the size of the maximum clique and the size of the solution we've found at that point is larger than the logarithm of the graph size, we'd have exponential behavior.

I believe this is not possible. I've tried manually crafting such a graph, but I haven't been able.

alx‭ wrote 5 months ago

(2)

Once step 8 finishes, it prints the maximum clique that contains X, from the graph with which we started the recursion level. It is not an n-clique, but actually the maximum clique that contains X. We don't need to try again with X at that level of recursion (but we will try it at levels that don't descend from this branch).

alx‭ wrote 5 months ago · edited 5 months ago

(re: further thought)

Yeah, I have this issue. Most resources are about 3SAT, and relatively few people work on the maximum clique. This makes it a bit difficult to check whether something is correct or not, other than trying hard myself. :)

alx‭ wrote 5 months ago · edited 5 months ago

About trying to design such a hypothetical worst case, I think it would have to be a smooth graph where the number of edges goes from very low at one end to almost 1 at the other end.

But that looks precisely like the best case where we have shown that p grows smoothly.

That's why I believe this hypothetical worst case is self-contradicting, and thus impossible.

clemens‭ wrote 5 months ago

@alx: Interesting; those kinds of "crucial sophistications" in the algorithm are what I was trying to find. But with the original algorithm you showed, it still seems to me that the algorithm will have to go through an $O(exp(cn))$ process to prove that there doesn't exist an $n+1$-clique in the $n$-dimensional hyperoctahedral graph; so I'm probably missing something about the elimination process…

Intuitively, how does the basic quasi-polynomial algorithm you described in the OP avoid searching through an exponential amount of trials to eliminate this $n+1$-clique?

alx‭ wrote 5 months ago

clemens‭:

Interesting; those kinds of "crucial sophistications" in the algorithm are what I was trying to find.

Actually, I was wondering if that optimization was crucial to the algorithm being in QP. I thought not, but I'm not entirely sure.

But with the original algorithm...

I'll resolve it by hand, and omitting the optimizations not declared in the original algorithm.

$ cat not_so_high_p.txt 
11110011
11110011
11111100
11111100
00111111
00111111
11001111
11001111

Pick node 0 as X. ... We get the base solution as 0,1,2,3, then we reach for the first time step 5 with termination.

We get back at the previous level and remove 3 from P. That results in:

$ cat not_so_high_p.txt | npc-subset 8 0 1 2 | npc-clear 3
11100000
11100000
11100000
00000000
00000000
00000000
00000000
00000000

This collapses due to Z, and we go back to the previous level...

alx‭ wrote 5 months ago · edited 4 months ago
$ cat not_so_high_p.txt | npc-subset 8 0 1 | npc-clear 2
11010011
11010011
00000000
11010000
00000000
00000000
11000011
11000011

Here, nodes 3,6,7 collapse due to Z, and that causes 0,1 to also collapse (and anyway, AT only contained 6,7, which had already collapsed). So we go back to the previous level.

$ cat not_so_high_p.txt | npc-subset 8 0 | npc-clear 1
10110011
00000000
10110000
10110000
00000000
00000000
10000011
10000011

Similarly, this collapses in the order 2,3,6,7,0, and we go back to the upper-most level.

$ cat not_so_high_p.txt | npc-clear 0
00000000
01110011
01111100
01111100
00111111
00111111
01001111
01001111

Okay, now I'm not allowed to use the undeclared optimization. AT contains nodes 4,5:

$ cat not_so_high_p.txt | npc-alt 8 0
00000000
00000000
00000000
00000000
00001100
00001100
00000000
00000000

We pick 4 arbitrarily, since they are equally good. ...

alx‭ wrote 5 months ago · edited 4 months ago
$ cat not_so_high_p.txt | npc-clear 0 | npc-subset 8 4
00000000
00000000
00111100
00111100
00111111
00111111
00001111
00001111

This collapses in step 2bis, since nodes 2,3,6,7 all have only 3 edges, but we already know a solution of size 4; after that, nodes 4,5 also collapse since they only have 1 edge (after having removed the others).

Then, we go back to the upper-most level, and remove node 4 from A (step 13), which leaves only node 5 in A. Having only one node, step 14 removes it, and we have an empty A. At this point, when we reach step 5 in the upper-most level, we've ended the algorithm.

We've only iterated once in the upper-most level. This is clearly far from exponential; not even QP.

clemens‭ wrote 5 months ago · edited 5 months ago

@alx: Hmm. Sorry, I'm still not grasping this.

Let us say that $f(n)$ is the amount of time it takes to show that there is no $n+1$-clique in an $n$-dimensional hyperoctahedron. (E.g. below is the hyperoctahedral graph of dimension 5 (so it'll take $f(5)$ steps to show no 6-clique):)

1111101111
1111110111
1111111011
1111111101
1111111110
0111111111
1011111111
1101111111
1110111111
1111011111

Then, as you said, we start by choosing an arbitrary trial point $X$ (e.g. 0). Then we prove that there is no $n$-clique in the $n-1$-dimensional hyperoctahedron of points touching $X$ — this takes $f(n-1)$ steps. Having done so, we eliminate $X$ from our hyperoctahedron and show that there is no $n+1$-clique in the graph that results. But—with the original, unoptimized algorithm, this takes at least $f(n-1)$ steps, because it too requires showing that an $n-1$-dimensional hyperoctahedron lacks an $n$-clique (choose $n+X \pmod{2n}$ as trial point).

Hence $f(n) ≥ f(n-1) + f(n-1)$…

alx‭ wrote 5 months ago · edited 4 months ago

Okay, now I understand your doubt, I think.

For reducing the complexity of proving that there's no 6-clique, you need to remember which node you used as trial point when you found the 5-clique. That's essential.

Let's say you found the 5-clique by choosing node 9 as a trial point. We know that the maximum clique that contains node 9 has size 5. If we want to find a better clique (i.e., a 6-clique), it must not contain node 9. Thus, we can remove node 9 from our graph. Moreover, we know that the nodes connected to 9 (excluding 9 itself) can only form a 4-clique. Thus, a 6-clique will need at least two nodes that are not connected to 9. Since there was only one node not connected to 9, that's not possible.

alx‭ wrote 4 months ago · edited 4 months ago

So, the algorithm doesn't escalate from cliques of size 1,2,3,... until the maximum clique.

It escalates from the smallest (or one of the smallest, at least) local maximum clique, to a nearby local maximum clique, ..., to the absolute maximum clique.

Finding a small local maximum clique is easy (polynomic). Once we reach that, we always hold a local maximum clique, which gives us a lot of information to find a nearby one that is larger. The information doesn't come only from the local maximum clique, but from the path we've taken to find it; the path itself is very informative.

Choosing wisely the order of the search of local maxima is what makes the algorithm (I believe) QP. The information given by each local maximum and the path used to find it is slightly different, and their cost too, and one needs to find a way to maximize the information without incurring in too much cost. This is the difficult part, I think.

clemens‭ wrote 4 months ago

@alx: Interesting. So this is a dynamic-programming-style approach to eliminating unviable possibilities! But where is it in the algorithm you posted?

alx‭ wrote 4 months ago · edited 4 months ago

clemens‭

That info is stored in the graph A (and AT, which is a temporary graph that may become A). Graph A is constructed in steps 12 and 13 --by then, named AT-- by removing all nodes connected to X (and X itself). The name A comes from '(viable) Alternatives (to X)'.

Since a better local maximum clique must contain at least two nodes not connected to X, we force a start with one that is not connected to X (thus, the requirement in step 4 that the candidate must exist in A).

And step 14 is because we need not only one node unconnected to X, but two, and of course, those two must be connected among themselves.

Skipping 1 deleted comment.

clemens‭ wrote 4 months ago

@alx: Very interesting. So that step looks like it will rule out any "Turán-graph"-style counterexamples…

alx‭ wrote 4 months ago · edited 4 months ago

@clemens: After looking at images of such graphs, yes, I think they would be trivial (polynomial) to solve with this algorithm. (I haven't tried.)

alx‭ wrote 4 months ago · edited 4 months ago

clemens‭. Now that you understand the basic idea of the algorithm, do you know if anyone has tried this or something very similar before?

Something that has had me thinking for a long time that I can't have solved it is that this seems too obvious and simple to me, and that someone else must have tried it before and proved it wrong.

clemens‭ wrote 4 months ago

@alx That's what I'm surprised not to have found quite yet (though I haven't looked very much yet for material on the maximum clique problem specifically, and I didn't understand this particular element of the algorithm). It seems to me there's somehow a paucity of information on such problems.