0

I have a 3D array. I need to remove any elements that are in the same row, column position but on the next page (3rd dimension), and only use the first occurrence at that position. So if all pages were to multiply the result would be 0.

Since the 3D array may be of any size, I can't hard code solutions like isMember. I also can't use unique because elements can be the same, just not share the same position.

For example, input:

A(:,:,1) = [ 1 0 2];
A(:,:,2) = [ 1 1 0];
A(:,:,3) = [ 0 1 0];

the desired output is:

A(:,:,1) = [ 1 0 2];
A(:,:,2) = [ 0 1 0];
A(:,:,3) = [ 0 0 0];

How can I accomplish this?

Adriaan
  • 17,741
  • 7
  • 42
  • 75
  • Adding the explicit requirement to not use loops is A) based on a false premise (since the JIT overhaul loops are no longer slow), and B) invalidates existing answers. If you want a loop free solution, please first verify whether the answer provided works for your case, then try to vectorise it yourself. Stack Overflow is not a free coding service. – Adriaan Sep 12 '19 at 10:14

3 Answers3

2

Not the most elegant, but at least it works.

A(:,:,1) = [ 1 0 2 ];
A(:,:,2) = [ 1 1 0 ];
A(:,:,3) = [ 0 1 0 ];

for ii = 1:size(A,1)
    for jj = 1:size(A,2)
        unique_el = unique(A(ii, jj, :)); % Grab unique elements
        for kk = 1:numel(unique_el)
            idx = find(A(ii,jj,:) == kk);  % Contains indices of unique elements
            if numel(idx) > 1  % If an element occurs more than once
                A(ii, jj, idx(2:end)) = 0; % Set to 0
            end
        end
    end
end
A
A(:,:,1) =
     1     0     2
A(:,:,2) =
     0     1     0
A(:,:,3) =
     0     0     0

I loop over the first two dimensions of A (rows and columns), find any unique elements which occur on a certain row and column location through the third dimensions (pages). Then set all occurrences of a unique element after the first to 0.

Given a more elaborate 3D matrix this still works:

A(:,:,1) = [1 0 2 0; 2 1 3 0];
A(:,:,2) = [1 1 0 0; 2 2 1 0];
A(:,:,3) = [0 1 1 3; 1 2 2 4]; 
A(:,:,1) =
     1     0     2     0
     2     1     3     0
A(:,:,2) =
     0     1     0     0
     0     2     1     0
A(:,:,3) =
     0     0     1     3
     1     0     2     4

If you want the first non-zero element and discard any element occurring afterwards, simply get rid of the unique() call:

A(:,:,1) = [1 0 2 0; 2 1 3 0];
A(:,:,2) = [1 1 0 0; 2 2 1 0];
A(:,:,3) = [0 1 1 3; 1 2 2 4];


for ii = 1:size(A,1)
    for jj = 1:size(A,2)
        idx = find(A(ii,jj,:) ~= 0);  % Contains indices of nonzero elements
        if numel(idx) > 1  % If more than one element
            A(ii, jj, idx(2:end)) = 0; % Set rest to 0
        end
    end
end
A(:,:,1) =
     1     0     2     0
     2     1     3     0
A(:,:,2) =
     0     1     0     0
     0     0     0     0
A(:,:,3) =
     0     0     0     3
     0     0     0     4
Adriaan
  • 17,741
  • 7
  • 42
  • 75
2

My solution assumes, that, for a given "position", EVERY value after the first occurence of any value is cleared. Some of the MATLAB regulars around here had some discussions on that, from there comes the "extended" example as also used in Adriaan's answer.

I use permute and reshape to rearrange the input, so that we have all "positions" as "page" columns in a 2D array. Then, we can use arrayfun to find the proper indices of the first occurence of a non-zero value (kudos to LuisMendo's answer here). Using this approach again, we find all indices to be set to 0.

Let's have a look at the following code:

A(:,:,1) = [1 0 2 0; 2 1 3 0];
A(:,:,2) = [1 1 0 0; 2 2 1 0];
A(:,:,3) = [0 1 1 3; 1 2 2 4]

[m, n, o] = size(A);

B = reshape(permute(A, [3 1 2]), o, m*n);

idx = arrayfun(@(x) find(B(:, x), 1, 'first'), 1:size(B, 2));
idx = arrayfun(@(x) find(B(idx(x)+1:end, x)) + idx(x) + 3*(x-1), 1:size(B, 2), 'UniformOutput', false);
idx = vertcat(idx{:});
B(idx) = 0;

B = permute(reshape(B, o, m , n), [2, 3, 1])

Definitely, it makes sense to have a look at the intermediate outputs to understand the functioning of my approach. (Of course, some lines can be combined, but I wanted to keep a certain degree of readability.)

And, here's the output:

A =

ans(:,:,1) =
   1   0   2   0
   2   1   3   0

ans(:,:,2) =
   1   1   0   0
   2   2   1   0

ans(:,:,3) =
   0   1   1   3
   1   2   2   4

B =

ans(:,:,1) =
   1   0   2   0
   2   1   3   0

ans(:,:,2) =
   0   1   0   0
   0   0   0   0

ans(:,:,3) =
   0   0   0   3
   0   0   0   4

As you can see, it's identical to Adriaan's second version.

Hope that helps!

HansHirse
  • 18,010
  • 10
  • 38
  • 67
2

A vectorized solution. You can use the second output of max to find the index of the first occurence of a nonzero value along the third dimension and then use sub2ind to convert that to linear index.

A(:,:,1) = [ 1 0 2];
A(:,:,2) = [ 1 1 0];
A(:,:,3) = [ 0 1 0];
[~, mi] =max(logical(A) ,[], 3);
sz=size(A) ;
[x, y] =ndgrid(1:sz(1),1:sz(2));
idx=sub2ind( sz, x,y,mi);
result=zeros(sz) ;
result(idx) =A(idx);
rahnema1
  • 15,264
  • 3
  • 15
  • 27