简体   繁体   中英

how to find multidimensional array permutation in php

I'm trying to fix this riddle:

I have an array called input :

$input = [
    'A' => ['X', 'Y', 'Z'],
    'B' => ['X', 'Y', 'Z'],
    'C' => ['X', 'Y', 'Z'],
];

I have to do some magic on it that the output array have unique value on same key on each array, may looks easy but is not, this magic should add empty string if there is no match for cell, only rule is to have unique value on same key on each inner array.

$output = [
    'A' => ['X', 'Y', 'Z'],
    'B' => ['Z', 'X', 'Y'], 
    'C' => ['Y', 'Z', 'X'], 
];

The rules are:

  1. output array must contain arrays with same values from input array,
  2. wildcard are only allowed if there is no valid match for given index
  3. different inner array sizes are allowed
  4. inner array values can't duplicate.
  5. output array must have unique values on the same key

example:

$input = [
    'A' => ['X', 'Y'],
    'B' => ['X', 'Y'],
    'C' => ['X', 'Y'],
];

possible solution:

$output = [
    'A' => ['X', 'Y', ''],
    'B' => ['', 'X', 'Y'],
    'C' => ['Y', '', 'X'],
];

TL;DR: This code compactly solves the case where all rows contain all values in any order. For a solution to the general case - every row has a sub-set of all possible values - see my other answer .


“If I had only one hour to solve a problem, I would spend up to two-thirds of that hour in attempting to define what the problem is.” -- Albert Einstein

Seems like you want a ragged array squared up with no columns sharing the same ordering. Let's define functions for that:

function array_make_square(array &$array, $fill = ' ') {
    $pad = count(array_keys($array));
    foreach ($array as &$row) {
        $row = array_pad($row, $pad, $fill);
    }
}

function array_organize(array &$outer) {
    $offset = 0;
    foreach ($outer as &$inner) {
        sort($inner);
        array_rotate($inner, $offset++);
    }
}

function array_rotate(array &$array, $offset) {
    foreach (array_slice($array, 0, $offset, true) as $key => $val) {
        unset($array[$key]);
        $array[$key] = $val;
    }
}

Now let's put some wrappers around those to test the behavior:

function solve($test) {
    array_make_square($test);
    array_organize($test);
    return $test;
}

function display($outer) {
    echo "[\n";
    foreach ($outer as $row => $inner) {
        echo "  $row => ['" . implode("', '", $inner) . "']\n";
    }
    echo "]\n";
}

$tests = [
    [ ['X'], ['X'], ['X'] ],
    [ ['X', 'Y'], ['X', 'Y'], ['X', 'Y'] ],
    [ ['X', 'Y', 'Z'], ['X', 'Y', 'Z'], ['X', 'Y', 'Z'] ],
];
array_map('display', array_map('solve', $tests));

Which produces:

[
  0 => [' ', ' ', 'X']
  1 => [' ', 'X', ' ']
  2 => ['X', ' ', ' ']
]
[
  0 => [' ', 'X', 'Y']
  1 => ['X', 'Y', ' ']
  2 => ['Y', ' ', 'X']
]
[
  0 => ['X', 'Y', 'Z']
  1 => ['Y', 'Z', 'X']
  2 => ['Z', 'X', 'Y']
]

See it live on 3v4l.org.

TL;DR: Square the matrix, then use brute force to generate every possible column rotation. When one rotation has no duplicate column values, stop. See it live on 3v4l.org.


My other answer provides a compact solution if and only if all rows contain all possible values (in any order). Here's an example. My previous answer guarantees a solution to the matrix on the left, but not the one on the right:

Satisfies              Does not satisfy
[                      [
   [ 'X', 'Y' ],           [ 'X', 'Y' ],
   [ 'Y', 'X' ],           [ 'Y' ],
]                      ]

To handle the general case where any row may contain any arbitrary sub-set of all possible values, we can brute force a solution. Our brute needs to know how to:

  1. Check if the matrix is solved.
  2. Systematically evolve the matrix toward a solution.

The first one is easy to write. For every column in a given a square matrix , check if the number of unique, non-wildcard values differs from the number of values. If so, then at least one non-wildcard value is duplicated in that column:

function matrix_is_solved(array $matrix) {
    foreach (array_keys(current($matrix)) as $offset) {
        $column = array_filter($raw = array_column($matrix, $offset));
        if (count($column) != count(array_unique($column))) return false;
    }
    return true;
}

The second part requires a trick. Observe that one can apply a vector to any square matrix to rotate values in the matrix. I'll skip the math and just show some examples:

original = [
   [ 'X', 'Y' ],
   [ 'Y', 'X' ],
]

vector = [ 0, 0 ] // rotate 1st row 0 places, 2nd row 0 places
result = [
   [ 'X', 'Y' ],
   [ 'Y', 'X' ],
]

vector = [ 1, 0 ] // rotate 1st row 1 place, 2nd row 0 places
result = [
   [ 'Y', 'X' ],
   [ 'Y', 'X' ],
]

vector = [ 0, 1 ] // rotate 1st row 0 places, 2nd row 1 place
result = [
   [ 'X', 'Y' ],
   [ 'X', 'Y' ],
]

vector = [ 1, 1 ] // rotate 1st row 1 place, 2nd row 1 place
result = [
   [ 'Y', 'X' ],
   [ 'X', 'Y' ],
]

Since there are 2 rows of 2 columns each, there are exactly 4 combinations of rotation vectors (all listed above). Of these four vectors, two - [ 0, 0 ] and [ 1, 1 ] - produce a solution. Since we only need one solution, it's sufficient to stop on the first vector that produces a solved matrix.

Knowing this, here's how we'll write our brute: generate all possible rotation vectors, try each one against our puzzle, and return if the transformed matrix is in a solved state:

function matrix_generate_vectors(array $matrix) {
    $vectors = [];
    $columns = count(current($matrix));
    $gen = function ($depth=0, $combo='') use (&$gen, &$vectors, $columns) {
        if ($depth < $columns)
            for ($i = 0; $i < $columns; $i++)
                $gen($depth + 1, $i . $combo);
        else
            $vectors[] = array_map('intval', str_split($combo));
    };
    $gen();
    return $vectors;
}

function matrix_rotate(array $matrix, array $vector) {
   foreach ($matrix as $row => &$values) {
       array_rotate($values, $vector[$row]);
   }
   return $matrix;
}

function matrix_brute_solve(array $matrix) {
    matrix_make_square($matrix);
    foreach (matrix_generate_vectors($matrix) as $vector) {
        $attempt = matrix_rotate($matrix, $vector);
        if (matrix_is_solved($attempt))
            return matrix_display($attempt);
    }
    echo 'No solution';
}

We'll also need some helpers, a test harness, and some tests:

function array_rotate(array &$array, $offset) {
    foreach (array_slice($array, 0, $offset) as $key => $val) {
        unset($array[$key]);
        $array[$key] = $val;
    }
    $array = array_values($array);
}

function matrix_display(array $matrix = null) {
    echo "[\n";
    foreach ($matrix as $row => $inner) {
        echo "  $row => ['" . implode("', '", $inner) . "']\n";
    }
    echo "]\n";
}

function matrix_make_square(array &$matrix) {
    $pad = count(array_keys($matrix));
    foreach ($matrix as &$row)
        $row = array_pad($row, $pad, '');
}

$tests = [
    [ ['X'], ['X'] ],
    [ ['X'], ['X'], ['X'] ],
    [ [ 'X', '' ], [ '', 'X' ] ],
    [ ['X', 'Y', 'Z'], ['X', 'Y'], ['X']],
    [ ['X', 'Y'], ['X', 'Y'], ['X', 'Y'] ],
    [ ['X', 'Y', 'Z'], ['X', 'Y', 'Z'], ['X', 'Y', 'Z'] ],
    [ ['X', 'Y', 'Z', 'I', 'J'], ['X', 'Y', 'Z', 'I'], ['X', 'Y', 'Z', 'I'], ['X', 'Y', 'Z', 'I'], ['X', 'Y', 'Z'], ['X', 'Y', 'Z'] ],
];
array_map(function ($matrix) {
    matrix_display($matrix);
    echo "solved by:" . PHP_EOL;
    matrix_brute_solve($matrix);
    echo PHP_EOL;
}, $tests);

Which produces for those tests (just a few examples):

[
  0 => ['X', 'Y', 'Z']
  1 => ['X', 'Y', 'Z']
  2 => ['X', 'Y', 'Z']
]
solved by:
[
  0 => ['Z', 'X', 'Y']
  1 => ['Y', 'Z', 'X']
  2 => ['X', 'Y', 'Z']
]

[
  0 => ['X', 'Y', 'Z', 'I', 'J']
  1 => ['X', 'Y', 'Z', 'I']
  2 => ['X', 'Y', 'Z', 'I']
  3 => ['X', 'Y', 'Z', 'I']
  4 => ['X', 'Y', 'Z']
  5 => ['X', 'Y', 'Z']
]
solved by:
[
  0 => ['', 'X', 'Y', 'Z', 'I', 'J']
  1 => ['', '', 'X', 'Y', 'Z', 'I']
  2 => ['I', '', '', 'X', 'Y', 'Z']
  3 => ['Z', 'I', '', '', 'X', 'Y']
  4 => ['Y', 'Z', '', '', '', 'X']
  5 => ['X', 'Y', 'Z', '', '', '']

See it live at 3v4l.org.

The vector generator is O(n n ) in both time and space. So, if you had a 3x3 square matrix, there would be 3 3 = 27 iterations and stored array elements. For 4x4, you'd have 4 4 = 256. You get the idea. The generator can be improved to be O(1) in space , but being that it's a depth-first algorithm, it'll always be O(n n ) in time.

Also, as implemented, the algorithm stops when it finds the first solution. A trivial modification would allow you to find all solutions. An interesting addition to this puzzle would be to find the solution with the fewest rotations.

I am collecting all the info first ie key names like ABC etc., all values like XYZ etc.. and number of elements per each sub array. Then I create new arrays for each position in the sub array namely pos0, pos1 and pos2 in this case. Then I populate the new sub arrays using the values from valArray ( randomly picking), keeping the constraint that the value must not be repeated in this array or in any other array for the same position.

Hope this helps..

    <?php

    $input = [
        'A' => ['X', 'Y', 'Z','P'],
        'B' => ['X', 'Y', 'Z','Q'],
        'C' => ['X', 'Y', 'Z','R'],
        'D' => ['X','Y','K']
    ];

    // declare two arrays - keyArray will hold all key names like A, B, C
    // valArray will hold all the distinct values like X, Y, Z
    // valLength is number of elements in each sub array

    $keyArray = [];
    $valArray = [];
    $valLength =0; // no of elements in sub array       

    echo "<table width='100%'> <tr> <td width='20%'>&nbsp; </td> <td width='30%'>";
    // this loop will gather all the relevenat info for $keyArray, $valAray and $valLength
    echo "INPUT <br><br>";
    foreach($input as $key => $val) {
        if(is_array($val)) {
            echo "$key <br/>";
            $keyArray[] = $key;
            if(count($val) > $valLength) {
                $valLength = count($val);    
            }

            foreach($val as $key1 => $val1) {
                echo "$key1 => $val1<br>";
                if (! in_array($val1, $valArray)) {
                    //echo "no ...";
                    $valArray[] = $val1;
                }
            }
            echo "<br>----<br>"; 
        }
        else {
           // echo "$key => $val<br>";    
        }            
    }
    echo "</td><td width='30%'>";

    $output =[];

    //create arrays for each position

    for($i=0; $i<$valLength; $i++) {
            $posArr = "pos" . $i;
            $$posArr = [];            
    }

    foreach($keyArray as $key) {
        //echo "$key <br/>";
        $thisArr = [];
        for($i=0; $i<$valLength; $i++) {
            //echo "$i <br>";
            $posArr = "pos" . $i;

            $whileIterations = 0; // no of random iterations to perform before using wildcard 

            $randomKey=array_rand($valArray);
            $new = $valArray[$randomKey];                
            do {
                $randomKey=array_rand($valArray);
                $new = $valArray[$randomKey];  
                $whileIterations++;
                if($whileIterations > 10) { // no of iterations ls limited to 10
                    $new = '';
                    break;
                }
            }while(in_array($new,$thisArr) || in_array($new, $$posArr));

            $thisArr[] = $new; 
            //$$posArr[] = $new;
            if($new != '') {
                array_push($$posArr,$new); 
                // keep this in position array so that same value is not repeated in  the same position  
            }                
        }
        // now one subarray is ready to be assigned
        $keyName = $key;
        $$keyName = [];
        $$keyName = $thisArr;    
    }

    // push sub arrays into output array
    foreach($keyArray as $key) {
        $keyName = $key;  
        //echo "$keyName <br>";
        foreach ($$keyName as $key2) {
           // echo "$key2 <br/>";
        }
        //echo "<br>----<br>";            
        //array_push($output, $$keyName);
        $output[$keyName] = $$keyName;
    }

    echo "OUTPUT <br/><br/>";
    foreach($output as $key => $val) {
        if(is_array($val)) {
            echo "$key <br/>";
            $keyArray[] = $key;               
            foreach($val as $key1 => $val1) {
                echo "$key1 => $val1<br>";
            }
        }
        echo "<br>----<br>"; 

    }

    echo "</td><td width='20%'>&nbsp; </td></tr></table>";

    ?>

You can use array_shift to make a array with all the elements. Then set the lost element to '' . Below code is well commented. https://eval.in/895863

<?php
$ks = ['A', 'B', 'C', 'D'];
$vs = ['X', 'Y', 'Z', 'A'];

$input = [
    'A' => ['X', 'Z'],
    'B' => ['X', 'Z'],
    'C' => ['X', 'Z'],
];

// element not in $inpupt
$diff = array_diff($vs, $input[$ks[0]]);

// array with all the values
$arr = [];
foreach($ks as $k)
{
    $arr[$k] = $vs;
    array_push($vs, array_shift($vs));
}

// set value in the $diff array to ''
$result = array_map(function($value) use ($diff) {
    return array_map( function($v) use ($diff) {
        return in_array($v, $diff) ? '' : $v;
    }, $value);
}, $arr);


print_r($result);

Edit for OP's comment

For the $input with different element, the $diff array should calculated seperately. https://eval.in/896322

<?php
$ks = ['A', 'B', 'C', 'D', 'E'];
$vs = ['X', 'Y', 'Z', 'A'];

$input = [
    'A' => ['X', 'Z'],
    'B' => ['X', 'Z'],
    'C' => ['X', 'A'],
];

// array with all the values
$arr = [];
foreach($ks as $k)
{
    $arr[$k] = $vs;
    array_push($vs, array_shift($vs));
}

// set value in the $diff array to ''
$result = array_map(function($value, $k) use ($input, $vs) {

    // element not in $inpupt[$k]
    $diff = array_diff($vs, isset($input[$k]) ? $input[$k] : []);
    return array_map( function($v) use ($diff) {
        return in_array($v, $diff) ? '' : $v;
    }, $value);
}, $arr, $ks);

// when use multi array for `array_map` the index lost, so combine index with the result here.
$result = array_combine($ks, $result);

print_r($result);

ok i think we are close now i have this idea, there is one thing left to do, described it on the end

  1. increase size of input array to number of element of highest occurrences or highest length of child depends what is bugger
  2. try to find all placaes for X , then trying to all places for Y and so on.
  3. every time when its find add it to array that keeps used elements to prevents duplicates.

helpers:

function getHeight($array) {
    $max2 = $max1 = 0;
    $max = [];
    foreach ($array as $k => $v) {
        if ($max2 < count($v)) {
            $max2 = count($v);
        }
        foreach($v as $elem) {
            if(isset($max[$elem])) {
                $max[$elem]++;
        } else {
            $max[$elem] = 1;
        }
    }

    }
    foreach($max as $m) {
        if ($max1 < $m) {
            $max1 = $m;
        }
    }
    return max($max2, $max1);
}

function getRow($array, $j)
{
    $res = [];
    foreach ($array as $k => $v) {
        $res[] = $v[$j] ?? '';
    }
    return $res;
}

code

function solve_this_array_please() {
    $input = [
        'A' => ['X', 'Y', 'Z', 'I'],
        'B' => ['X', 'Y', 'Z', 'I'],
        'C' => ['X', 'Y', 'Z', 'I'],
        'D' => ['X', 'Y', 'Z', 'I'],
    ];

    usort($input,  function($i, $j) {
        return count($i) <=> count($j);
    });

    $output = [];

    $h = getHeight($input);

    // filling te missing wildcards;
    foreach($input as $k => $v) {
        for($i=0;$i<$h;$i++) {
            if (!isset($input[$k][$i]))
                $input[$k][$i] = '';
        }
        $output[$k] = [];
        $used[$k] = [];
    }

    $j = 0;
    foreach($input as $key2 => $col2) {
        foreach($col2 as $k => $elem) {

            foreach($input as $key => $col) {
                for($i=0;$i<$h;$i++) {
                    dump("TRYING: $key $i = $elem");
                    if (!in_array($elem, getRow($output, $i)) && !in_array($elem, $used[$key]) && (!isset($output[$key][$i]) || $output[$key][$i] == '')) {

                        if (in_array($elem, $col)) {
                            dump("ok");
                            $output[$key][$i] = $elem;
                            $used[$key][] = $elem;
                            $i = 0;
                            break;
                        }
                    }
                }

            }
        }
    }



    foreach($output as $k => $o) {
        ksort($output[$k]);
    }


    dump($output);
}

one thing I still need help with is to make that script to start searching next array on key when he find last one instead of 0

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM