I’m old enough to remember what we did before broadcasting, and that was to use .expand. Expand will create a view(!) with stride 0 for the expanded dimension, so it is much more efficient that repeat. (Personally, I don’t think I remember when I last saw a use of repeat that should not be expand instead.)
Y = X.gather(1, P.unsqueeze(-1).expand(-1, -1, c))

Gather is the most elegant way to do it by far, but there also is the option of explicitly indexing which might be the most immediate transcription of the loop you wrote:
i0 = torch.arange(a)[:, None, None].expand(-1, b, c)
i1 = P.unsqueeze(-1).expand(-1, -1, c)
i2 = torch.arange(c)[None, None, :].expand(a, b, -1)
print((X[i0, i1, i2] == Y).all())

(But I don’t know where the third dimension came from, you can leave that out to get closer to the initial message.)
Best regards
Thomas