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)
Implementation source code
alx‭ wrote 4 months ago · edited 4 months ago

I have published a git repository with my implementation of this algorithm. It includes a few optimizations not mentioned here.

https://www.alejandro-colomar.es/src/alx/alx/npc/npc-clq-solver.git

http://www.alejandro-colomar.es/src/alx/alx/npc/npc-clq-solver.git

The TLS cert is self-signed (see https://www.alejandro-colomar.es/ssl). If you don't trust it, I recommend accessing it as plain HTTP.

You'll need GCC 16 to compile the source code. I've only tried it on Linux, BTW. By running make, you'll compile everything, and then sudo make install will install it, although you don't need that if you set up the $PATH appropriately. Then, it's still not documented, so see the comments in other sub-threads to figure out how to use it. I'll try documenting it soon.

The entry point is npc-solve, and the main logic is in npc-lvl.

npc-rand generates instances for npc-solve. npc-tr-dimacs translates DIMACS clq files to my format.

alx‭ wrote 4 months ago · edited 4 months ago
clemens‭ wrote 4 months ago

@alx: Out of curiosity—how did you write the "Cc:" comment without going under the character limit?

alx‭ wrote 4 months ago · edited 4 months ago

I added many spaces between 'Cc:' and your name. :)

(I had to do it after typing your name; otherwise, it didn't seem to work.)