Outer with non-diagonal elements

MapIndexed and Drop removes the diagonal:

MapIndexed[Drop, Outer[f, Range[3], Range[3]]]
(*  {{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}  *)

It removes the diagonal of any square array, for instance, if the array is generated with an f that is already defined or is a Function:

MapIndexed[Drop, Outer[10 #1 + #2 &, Range[3], Range[3]]]
(*  {{12, 13}, {21, 23}, {31, 32}}  *)

It can be used in the operator form MapIndexed[Drop] @ Outer[..].

It's faster than replacing diagonal elements or defining f[x, x] to evaluate to Nothing.

res1 = Block[{g = f},
     g[x_, x_] := Nothing; 
     Outer[g, Range[10^3], Range[10^3]]]; // RepeatedTiming // First
(*  0.42  *)

Clear[f];  (* needed if res1 is evaluated before res2 *)
res2 = Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; //
  RepeatedTiming // First
(*  0.43  *)

res3 = MapIndexed[Drop, Outer[f, Range[10^3], Range[10^3]]]; // 
  RepeatedTiming // First
(*  0.199  *)

res1 === res2 === res3
(*  True  *)

Most of the time for the Drop method is spent generating the array:

Outer[f, Range[10^3], Range[10^3]]; // RepeatedTiming // First
(*  0.184  *)

Note: The current method of using Block[{g = f},...] still defines a value for f[x_, x_], since g evaluates to f in SetDelayed. Failing to clear f before running res2 adds about 50% to the execution time (0.61s). Using Block[{f}, f[x_, x_] :=...] takes about 25% longer (0.506s).


Because operation on diagonal elements is performed anyways you could take an alternative path of removing them post-evaluation. You should ask a question what time is spent on the special construct removing diagonal during evaluation? Perhaps an alternative post-removal would be faster.

Replace[Outer[f, {1, 2, 3}, {1, 2, 3}], _[x_, x_] -> Nothing, {2}]

Timing measures:

Replace[Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; // AbsoluteTiming

{0.528235`, Null}

Outer[If[# == #2, Nothing, f@##] &, Range[10^3], Range[10^3]]; // AbsoluteTiming

{1.313204`, Null}

Array[If[# == #2, Nothing, f@##] &, {10^3, 10^3}]; // AbsoluteTiming

{1.41581`, Null}


Outer[If[# == #2, ## &[], f@##] &, {1, 2, 3}, {1, 2, 3}]

{{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}

Also

Array[If[# == #2, ## &[], f@##] &, {3, 3}]

{{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}

Note: You can also use Unevaluated @ Sequence and, in versions 10+, Nothing in place of ##&[].

Update: An alternative to post-processing suggested by VitaliyKaurov is to re-define f to give Nothing for equal arguments (f[x_,x_]:=Nothing) before using it in Outer:

res1 = Block[{g = f}, g[x_,x_]:=Nothing;Outer[g, Range[10^3], Range[10^3]]  ]; // 
    RepeatedTiming  // First  

0.56

res2 = Replace[Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; // 
    RepeatedTiming// First 

0.704

res1 == res2  

True