ComputeSum[A_, B_, M_] := A . Outer[KroneckerDelta, M, M] . B;
(*A helper function that for additive assembly of `SparseArray`s (_Mathematica's_ default is first in, last out.) *)
Options[MySparseArray] = {"Background" -> 0.};
MySparseArray[X_, r_, f_ : Total] :=
If[(Head[X] === Rule) && (X[[1]] === {}),
X[[2]],
With[{spopt = SystemOptions["SparseArrayOptions"]},
Internal`WithLocalSettings[
SetSystemOptions[
"SparseArrayOptions" -> {"TreatRepeatedEntries" -> f}],
SparseArray[X, r, OptionValue["Background"]],
SetSystemOptions[spopt]]
]
];
ComputeSum2[A_, B_, M_, k_] := Dot[
MySparseArray[Partition[M + k + 1, 1] -> A, {2 k + 1}],
MySparseArray[Partition[M + k + 1, 1] -> B, {2 k + 1}]
];
ComputeSum3[A_, B_, M_] := Dot[
Values[GroupBy[Transpose[{M, A}], First -> Last, Total]],
Values[GroupBy[Transpose[{M, B}], First -> Last, Total]]
];
n = 10000;
A = RandomReal[{-1, 1}, n];
B = RandomReal[{-1, 1}, n];
k = 6;
M = RandomInteger[{-k, k}, n];
result = ComputeSum[A, B, M]; // AbsoluteTiming // First
result2 = ComputeSum2[A, B, M, k]; //
AbsoluteTiming // First
result3 = ComputeSum3[A, B, M]; // AbsoluteTiming // First
Abs[result - result2]
Abs[result - result3]
Abs[result - result3]
16.8646
0.002846
0.006937
2.27374*10^-12
9.09495*10^-13
Edit
The idea of the two implementations is the same. We want to compute
$$\begin{aligned}
\sum_{i=1}^{n} \sum_{j=1}^n \alpha_i \, \delta_{M_i,M_j} \, \beta_j
&= \sum_{i=1}^{n} \sum_{j=1}^n \sum_{k=-6}^6 \alpha_i \, \delta_{M_i,k} \, \delta_{k,M_j} \, \beta_j
\\
&= \sum_{k=-6}^6 \left( \sum_{i=1}^{n}\alpha_i \, \delta_{M_i,k} \right) \, \left( \sum_{j=1}^n \delta_{k,M_j} \, \beta_j \right)
\\
& = u^T v,
\end{aligned}$$
where
$$
u_k = \sum_{i=1}^n \alpha_i \, \delta_{M_i,k}
\qquad
v_k = \sum_{j=1}^n \beta_j \, \delta_{M_j,k}.
$$
The naive summation costs $O(n^2)$; but each of u
and v
can be computed in $O((2\,k +1) \, n)$ time. So the new algorithm has complexity
$$
O(2\,(2\,k +1) \,n + (2\,k +1)) = O(2\,(2\,k +1)\,n).
$$
So if the range of k
is much smaller than n
, then we can save quite many flops this way.
Hence we may use
MySparseArray[Partition[M + k + 1, 1] -> A, {2 k + 1}]
(where we have to add shift the integers in M
to be all greater than 0
) or
Values[GroupBy[Transpose[{M, A}], First -> Last, Total]]
to assemble the vector u
. Likewise we can do it for v
. And in the end we just have Dot
u
and v
together to get the result.
Sum[..., {i, n}, {j, n}]
? $\endgroup$