This is an implementation of the Reservoir Sampling Algorithm. It will select exactly k out of n.



Overview:

The group node:

The Reservoir Sampling Algorithm
Here is the pseudo code from Wikipedia:
> (* S has items to sample, R will contain the result *)
> ReservoirSample(S[1..n], R[1..k])
> // fill the reservoir array
> for i := 1 to k
> R[i] := S[i]
>
> // replace elements with gradually decreasing probability
> for i := k+1 to n
> (* randomInteger(a, b) generates a uniform integer from the inclusive range {a, ..., b} *)
> j := randomInteger(1, i)
> if j <= k
> R[j] := S[i]
The algorithm starts with selecting the first k elements. R is an array, that stores the selection. S is the set of all points. Then, it goes through all other points and substitutes randomly one of the selected elements in R by the current point. In the end, R holds the selected points.
As indices in geometry nodes start with 0, we have to slightly modify this algorithm.
Implementing the Algorithm
Geometry nodes don’t provide a way, to store data in arrays. Thus, we need a substitute for it. Instead, we use a meshline with k points, to store the selection. I call them selection marker. Additionally, we create a meshline with n points, that represent the indices of the original points. To mark an index as selected, we place the corresponding point on the position of the selection marker. To be precise, we don’t place the index point exactly on the selection marker, but a little above. I will explain later, why we do it like this.
The following image shows the selection of the first k elements:

Next, we randomly replace the selected points by points from k+1 to n:

But geometry nodes do not allow us to change other elements, than the one we currently access through the context. This means, we can’t overwrite the already selected. Instead, we place every new selection closer to the selection marker than the ones before by using an Accumulate Field node. And this is the reason, why we did not place the selected index points exactly on the selection marker.

Now, that we have made our selection, we can place the selection marker points right on the selected original points. Therefore, we read the index from the index point, that is closest to the selection marker point and use this to read the position from the original point:

Finally, we can make our selection by choosing those original points, that have a selection marker assigned:
