Solving Sudoku in Python Packaging(github.com) |
Solving Sudoku in Python Packaging(github.com) |
Figuring out how it works is a great way to learn a bit more about how Python packaging works under the hood. I learned that .whl files contain a METADATA file listing dependency constraints as "Requires-Dist" rules.
I ran a speed comparison too. Using the uv pip resolver it took 0.24s - with the older pip-compile tool it took 17s.
Which is one of the reasons why uv is so fast. It reduces the total times it needs to go to PyPI! Not only does it cache really well, it also hits PyPI more efficiently and highly parallel. Once you resolved once, future resolutions will likely bypass PyPI for the most part entirely.
Of course we could've cached the venv, but cache invalidation is hard, and this is a very cheap way to avoid it.
The reason uv is fast is that it creates hard links from each of your virtual environments to a single shared cached copy of the dependencies (using copy-on-write in case you want to edit them).
This means that if you have 100 projects on your machine that all use PyTorch you still only have one copy of PyTorch!
Is that actually sufficient? Can every system that’s solving something that’s NP-complete solve every other NP-complete problem?
Now it is just an exhaustive, recursive search: for the current package try using versions from newest to oldest, enqueue its dependencies, if satisfied return, if conflict continue.
https://web.archive.org/web/20160326062818/http://algebraict...
Requires-Dist: sudoku_0_1 != 1
Requires-Dist: sudoku_0_2 != 1
Requires-Dist: sudoku_0_3 != 1
Requires-Dist: sudoku_0_4 != 1
Requires-Dist: sudoku_0_5 != 1
Requires-Dist: sudoku_0_6 != 1
Requires-Dist: sudoku_0_7 != 1
Requires-Dist: sudoku_0_8 != 1
Requires-Dist: sudoku_1_0 != 1
Requires-Dist: sudoku_2_0 != 1
Requires-Dist: sudoku_3_0 != 1
Requires-Dist: sudoku_4_0 != 1
Requires-Dist: sudoku_5_0 != 1
Requires-Dist: sudoku_6_0 != 1
Requires-Dist: sudoku_7_0 != 1
Requires-Dist: sudoku_8_0 != 1
Requires-Dist: sudoku_0_1 != 1
Requires-Dist: sudoku_0_2 != 1
Requires-Dist: sudoku_1_0 != 1
Requires-Dist: sudoku_1_1 != 1
Requires-Dist: sudoku_1_2 != 1
Requires-Dist: sudoku_2_0 != 1
Requires-Dist: sudoku_2_1 != 1
Requires-Dist: sudoku_2_2 != 1https://github.com/konstin/sudoku-in-python-packaging/blob/m...
Others have given the answer (yes) and provided some links. But it is nice to have an explanation in thread so I'll have a go at it.
The key idea is the idea of transforming one problem to another. Suppose you have some problem X that you do not know how to solve, and you've got some other problem Y that you do know how to solve.
If you can find some transform that you can apply to instances of X that turns them into instances of Y and that can transform solutions of those instances of Y back to solutions of X, then you've got an X solver. It will be slower than your Y solver because of the work to transform the problem and the solution.
Now let's limit ourselves to problems in NP. This includes problems in P which is a subset of NP. (Whether or not it is a proper subset is the famous P=NP open problem).
If X and Y are in NP and you can find a polynomial time transformation that turns X into Y then in a sense we can say that X cannot be harder than Y, because if you know how to solve Y then with that transformation you also know how to solve X albeit slower because of the polynomial time transformations.
In 1971 Stephen Cook proved that a particular NP problem, boolean satisfiability, could serve as problem Y for every other problem X in NP. In a sense then no other NP problem can be harder than boolean satisfiability.
Later other problems were also found that were universal Y problems, and the set of them was called NP-complete.
So if Python packaging is NP-complete then every other NP problem can be turned into an equivalent Python packaging problem. Note that the other problem does not have to also be NP-complete. It just has to be in NP.
Sudoku and Python Packaging both being NP-complete means it goes both ways. You can use a Python package solver to solve your sudoku problems and you can use a sudoku solver to solve your Python packaging problems.
Yes, by definition (https://en.wikipedia.org/wiki/NP-completeness , point 4).
The problem class of "Solve an arbitrary Sudoku of Size 9" might even be constant runtime, since it's a finite set to search through.
This video I think makes it obvious why that's true in a pretty intuitive way. I posted it a few days ago as a link and it never got traction.
SAT is the equivalent of being able to find the inverse of _any_ function, because you can describe any function with logic gates (for obvious reasons), and any collection of logic gates that describes a function is equivalent to a SAT problem. All you need to do is codify the function in logic gates, including the output you want, and the ask a SAT solver to find the inputs that produce that output.
(One of these days I'll have to figure out this "CI" thing. But my main focus is really just solving interesting programming problems, and making simple elegant tools for mostly-small tasks.)
(TIL `os.link` has been supported on Windows since 3.2.)
I suppose you could leave some blank and make the squares the next largest perfect square
I mean non-square sudokus are still in NP. If a solution can be validated in polynomial time, a problem is in NP.
And to validate a (x, y) sized sudoku, you need to check (x * y) [ number of total squares] * (x [row] + y [column] + max(x, y)ish [diagonal-ish] + ((x/3) + (y/3)) [box]). The boxes might be weird, but boxes are smaller than the overall sudoku, so we are looking at some max(x, y)^4 or so. Same for the diagonals. The input is x*y numbers, so max(x, y)^2. Very much polynomial in the size of the input[1]
And it should also be easy to show that if an (n, n) sized sudoku has a solution, an (n+k, n+k) sized sudoku has a solution. You kinda shove in the new numbers in knights-kinda moves and that's it.
1: this can be a bit weird, because you need to be careful "what you're polynomial in". If your input is a number or two, you might be polynomial in the magnitude of the number, which however is exponential with the input length.
In this case however, we wouldn't have encoding shenanigans, since we're just placing abstract symbols from the turing machine's alphabet onto an imagined grid.
Like, "Constant time" means, "Runtime independent from input". And, well, solving any sudoku of size 9 or less is a constant times 6.6e21. Maybe a bit more in the exponent, but meh.
Like in graph theory, there are some algorithms for I think maxflow, which solve the thing in O(node_count^4). Theory can push this down to like O(node_count^3) or O(node_count^2.7) or less. That's amazing - you can lose almost 2 orders of magnitude.
Well, implementation of these algorithms and more detailed analysis point out _huge_ precomputations necessary to achieve the speedups. In practice, you'd only see speedups if you had graphs with multiple billions of nodes. In practice, if you deal with a boring subset like "realistically relevant", asymptotically worse algorithms may be the objectively better choice.
Like in this case. Here, some O(n^5) - O(n^9) depending on what the solver does can be better than O(1) for many practical purposes.
In such areas, intuition is little more than a lie.