Так же, как вместо
sapply(1:5, `*`, 2)
# [1] 2 4 6 8 10
или цикл, который мы предпочитаем
1:5 * 2
# [1] 2 4 6 8 10
, поскольку это векторизованное решение, выполняющее точно такие же операции - поэлементное умножение,
rowSums(X %*% C * X)
# [1] 0.2484329 0.5583787 0.2303054
лучше, чем
apply(X, 1, function(row) t(row) %*% C %*% row)
# [1] 0.2484329 0.5583787 0.2303054
с тем, что оба они снова делают одно и то же, только с первым более кратким.
В частности, в моем первом примере мы перешли от скаляров к векторам, а теперь мы переходим от векторов к матрицам. Во-первых,
X %*% C
# [,1] [,2]
# [1,] 0.7611212 0.6519212
# [2,] -0.4293461 0.6905117
# [3,] 1.2917590 -1.2970376
соответствует
apply(X, 1, function(row) t(row) %*% C)
# [,1] [,2] [,3]
# [1,] 0.7611212 -0.4293461 1.291759
# [2,] 0.6519212 0.6905117 -1.297038
Теперь второй продукт в t(row) %*% C %*% row
делает две вещи: 1) поэлементное умножение t(row) %*% C
и row
, 2) суммирование. Таким же образом, *
в X %*% C * X
делает 1) и rowSums
делает суммирование, 2).
Итак, в этом случае нет существенных уловок по изменению порядка операций, разбиения или чего-либо еще; он просто использует преимущества существующих матричных операций, которые повторяют те же действия с каждой строкой / столбцом для нас.
Дополнительно:
library(microbenchmark)
microbenchmark(rowSums = rowSums(X %*% C * X),
apply = apply(X, 1, function(row) t(row) %*% C %*% row),
times = 100000)
# Unit: microseconds
# expr min lq mean median uq max neval cld
# rowSums 3.565 4.488 5.995129 5.117 5.589 4940.691 1e+05 a
# apply 24.126 26.402 32.539559 27.191 28.615 129234.613 1e+05 b