Number permutations in PHP with repeated characters - php

I store a number in a string. My code shuffles the digits into different permutations.
Example if the input is:
'123'
then the output permutations will be:
123,132,213,231,321,312
If the input string has repeated digits, my code does not work, and goes into an infinite loop.
Example inputs that don't work:
11,22,33,44,55,455,998,855,111,555,888,222 etc.
My code:
<?php
function factorial($n){
if($n==1) return $n;
return $n*factorial($n-1);
}
$a = '1234';
$_a = str_split($a);
$num = count($_a);
$ele_amnt = factorial($num);
$output = array();
while(count($output) < $ele_amnt){
shuffle($_a);
$justnumber = implode('', $_a);
if(!in_array($justnumber , $output))
$output[] = $justnumber;
}
sort($output);
print_r($output);
Can anyone explain why and how to fix it?

Short version: Your terminating condition for the while loop "is" permutational while your if(!in_array...) test "is" combinational.
Let's assume $a=11;: then $ele_amnt is 2 and your while loop will stop when the array $output contains more than one element.
Your shuffle/implode code can produce either the string <firstelement><seconelement> or <secondelement><firstelement>, both being 11.
And if(!in_array( $justnumber , $output)) allows only one of them to be appended to $output. So count($output) will be 1 after the first iteration and will stay 1 in perpetuity. Same for every $a with duplicate digits.
shuffle() changes the position of elements in an array at random. SO, the performance of the algorithm depends on ....luck ;-)
You might be interested in something like https://pear.php.net/package/Math_Combinatorics instead.

Your output array will contain less permutations if you have repeated characters in your input. So your loop never completes.
You could map your inputs, then later map back from your output, and then filter to do as you desire:
// For a string '122' we get the permutations of '123' first and then process.
$output = op_code_no_repeats('123');
$filtered = array();
foreach($output as $permutation) {
$filtered[] = str_replace('3', '2', $permutation);
}
$filtered = array_unique($filtered);
var_dump($filtered);
Outputs:
array (size=3)
0 => string '122' (length=3)
2 => string '212' (length=3)
3 => string '221' (length=3)
Your code with guards on the factorial and permutation functions:
function factorial($n)
{
if(! is_int($n) || $n < 1)
throw new Exception('Input must be a positive integer.');
if($n==1)
return $n;
return $n * factorial($n-1);
};
function op_code_no_repeats($a) {
$_a = str_split($a);
if(array_unique($_a) !== $_a)
throw new Exception('Does not work for strings with repeated characters.');
$num = count($_a);
$perms_count = factorial($num);
$output = array();
while(count($output) < $perms_count){
shuffle($_a);
$justnumber = implode('', $_a);
if(!in_array($justnumber , $output))
$output[] = $justnumber;
}
sort($output);
return $output;
}

Related

Find the sum of alphabetical characters in a string

I want to get the sum of all selected values from an array. If a user searches for a word it should return a sum for the searched word. Example below.
$chars = array(1 => 'a', 2 => 'b', 3 => 'c'); // Chars start from a-z
$data = 'aac'; // for example
foreach ($chars as $results) {
if (strpos($results, $data) !== false) {
$search = array_search($results, $chars);
}
}
If search is "aac" then the results should be 1 + 1 + 3 = 5.
I have tried the array_keys() function and it did not work.
You can split your $data into an array and loop over every character in the string. Search for the string in the array, and if it exists, then add the index you get from array_search() to a $sum variable.
$sum = 0;
foreach (str_split($data) as $char) {
if (($index = array_search($char, $chars)) !== false) {
$sum += $index;
}
}
echo $sum;
Live demo at https://3v4l.org/XCK8r
You can use str_split to create a character array, then accumulate lowercase alphabetical characters only into a sum. Use ord to get an ASCII code per letter instead of hardcoding a literal array (the array would only be useful if you want arbitrary weights per character; see other answers).
<?php
$data = "aac";
$result = array_reduce(str_split($data), function ($a, $e) {
return $a + (ctype_lower($e) ? ord($e) - 96 : 0);
}, 0);
echo $result; // => 5
If you wish to ignore case and treat "A" as "a", you can use strtolower.
<?php
$data = "aAc";
$result = array_reduce(str_split(strtolower($data)), function ($a, $e) {
return $a + (ctype_lower($e) ? ord($e) - 96 : 0);
}, 0);
echo $result; // => 5
To save having to search the array for the value of the character, instead use array_flip() to make it an array indexed by the character itself. Then just add the items up (using ?? in case the item doesn't exist to add 0)...
$sum = 0;
$chars = array_flip($chars);
foreach (str_split($data) as $char) {
$sum += $chars[$char] ?? 0;
}
echo $sum;

Convert array to decimal

I have this code:
public function get_names($number){
$names = array(
'None',
'Anton',
'bertha',
'Cesa',
'Dori',
'Egon',
'Frank',
'Gollum',
'Hans',
'Kane',
'Linus',
'Moose',
'Nandor',
'Oliver',
'Paul',
'Reese');
$bin = strrev(decbin($number));
$len = strlen($bin);
$output = array();
foreach(str_split($bin) as $key=>$char)
{
if ($key == sizeof($names)){
break;
}
if($char == 1)
{
array_push($output,$names[$key]);
}
}
return $output;
}
When I now call the function with number - let's say - 32256 I would get an array with "Kane, Linus, Moose, Nandor, Oliver, Paul".
Can anyone tell me what I would have to do when I want to give a certain names array and wanna get the number as a result where the certain bits are included? So exactly the other way around.
I found that code somewhere which works fine. But I need it vice versa.
Thanks in advance!
Andreas
EDIT: I want to know the decimal number when I e.g. have an array with "Anton, bertha,Cesa". I want to store those in a database instead of storing arrays with names each time. And when I need the names I just take the decimal number from database and use my function to get my name arrays.
If you just take the position in your $names array as being the bit position, you raise 2 to this position to give it the correct bit position and keep a running total of the items found...
$input = ["Cesa", "Gollum", "Hans"];
$output = 0;
foreach ( $input as $digit ) {
$output += pow(2,array_search($digit, $names ));
}
echo $output; // 392
with
$input = ["Kane", "Linus", "Moose", "Nandor", "Oliver", "Paul"];
gives
32256
Or as Barmar points out in his comment, you can save the lookup by inverting the names array using array_flip() which will mean that looking up each key will give the position in the array...
$output = 0;
$names = array_flip($names);
foreach ( $input as $digit ) {
$output += pow(2,$names[$digit]);
}
echo $output;

How to check if words can be created from list of letters?

I have a string $raw="aabbcdfghmnejaachto" and an array $word_array=array('cat','rat','goat','total','egg').
My program needs to check whether it is possible to make the words in the array with letters from the string. There is one extra condition; if the word contains a letter occurring more than once, that letter must occur at least the same number of times in the string.
E.g. egg. There are two g's. If the string $raw doesn't contain two g's, then it's not possible to make this word.
This is my expected result:
Array([cat]=>'Yes',[rat]=>'No',[goat]=>'Yes',[total]=>'No',[egg]=>'No')
I tried the following, but it doesn't output the expected result:
$res=array();
$raw="aabbcdfghmnejaachto";
$word_array=array('cat','rat','goat','total','egg');
$raw_array= str_split($raw);
foreach($word_array as $word=>$value)
{
$word_value= str_split($value);
foreach($word_value as $w=>$w_value)
{
foreach($raw_array as $raw=>$raw_value)
{
if(strcmp($w_value,$raw_value)==0)
{
$res[$value]='Yes';
}
else
{
$res[$value]='No';
}
}
}
}
print_r($res);
EDIT: The code, as originally posted, was missing the letter e from the string $raw so the egg example would actually return No. I have updated the Question and all the Answers to reflect this. - robinCTS
You must loop through each word/element in the $words array, then loop again through each character of each word.
Upon each iteration of the outer loop, set the default result value to Yes.
Then you must iterate each unique character of the current word. (array_count_values())
Check if the number of occurrences of the current character in the word is greater than the number of occurrences of the current character in the string of letters.
*As a matter of performance optimization, array_count_values() is used on the inner loop to avoid any unnecessary iterations of duplicate letters in $word. The $count variable saves having to make two substr_count() calls in the if statement.
Code: (Demo)
$string = "aabbcdfghmnejaachto";
$words = array('cat','rat','goat','total','egg');
foreach ($words as $word) { // iterate each word
$result[$word]='Yes'; // set default result value
foreach (array_count_values(str_split($word)) as $char=>$count) { // iterate each unique letter in word
if ($count > substr_count($string, $char)) { // compare current char's count vs same char's count in $string
$result[$word]='No'; // if more of the character in word than available in $string, set No
break; // make early exit from inner loop, to avoid unnecessary iterations
}
}
}
var_export($result);
This is the output :
array (
'cat' => 'Yes',
'rat' => 'No',
'goat' => 'Yes',
'total' => 'No',
'egg' => 'No',
)
BIG THANKYOU to mickmackusa for hijacking significantly enhancing this answer.
Your problem is you are not counting the number of times each character occurs in the $raw array, you are just checking each character in each of the words to see if that character exists in $raw. Unless you put in some form of counting, or else make a copy of $raw for each word and remove letters as they are used, you are not going to be able to do this.
I have counted occurrences of characters in string and compare that number of occurrence! You can find this answer working!!!
$res=array();
$raw="aabbcdfghmnejaachto"; //tgrel -- to make all yes
$res=array();
$word_array=array('cat','rat','goat','total','egg');
$raw_array= str_split($raw);
$count_raw = array_count_values($raw_array);
foreach($word_array as $value)
{
$word_value= str_split($value);
$newArray = array_count_values($word_value);
$res[$value]='yes';
foreach($newArray as $char=>$number){
if(!isset($count_raw[$char]) || $count_raw[$char]<$number){
$res[$value]='No';
break;
}
}
}
print_r($res);
Your error here is obvious, that you decided whether a value a word is accepted or not on individual tests of characters, while it should be based on the all the letter of the word , you don't need to precise both the key and value of an array if you need only its value
as in
foreach($word_array as $value)
then I've found that the use of the function in_array(), make the code much clearer
$res=array();
$raw="aabbcdfghmnejaachto";
$res=array();
$word_array=array('cat','rat','goat','total','egg');
$raw_array= str_split($raw);
foreach($word_array as $value)
{
$word_value= str_split($value);
$res[$value]='yes';
foreach($word_value as $w_value)
{
if (!in_array($w_value,$raw_array))
$res[$value]='No';
}
}
print_r($res);
Lets try to make it w/o loops, but with closures:
$raw = "aabbcdfghmnejaachto";
$word_array = ['cat', 'rat', 'goat', 'total', 'egg'];
$result = [];
$map = count_chars($raw, 1);
array_walk(
$word_array,
function ($word) use ($map, &$result) {
$result[$word] = !array_udiff_assoc(
count_chars($word, 1), $map, function ($i, $j) { return $i > $j; }
) ? 'Yes' : 'No';
}
);
We are building a map of symbols, used in original string with count_chars($raw, 1), so it will look like this.
$map:
[
97 => 4, // "97" is a code for "a"; and "4" - occurrence number.
98 => 2,
...
]
array_walk goes through words and adds each of them in a final $result with a Yes or No values that come from a comparison with a map, that was built for a word.
array_udiff_assoc compares two maps, throwing away those elements that have the same key and values bigger for an original map (comparing with a map for a word). Also array_udiff_assoc() returns an array containing all the values from array1 that are not present in any of the other arguments, so the final step is a negation operation preceding array_udiff_assoc.
Demo
Try this
$res=array();
$word_array=array('cat','rat','goat','total','egg');
$raw="aabbcrdfghmnejaachtol";
foreach($word_array as $word=>$value)
{
$raw_array= str_split($raw);
$res[$value]='Yes';
$word_value= str_split($value);
foreach($word_value as $w=>$w_value)
{
if(!in_array($w_value,$raw_array))
{
$res[$value]='No';
}
else
{
unset($raw_array[array_search($w_value, $raw_array)]);
}
}
}
This will not allow character again, if it is used once Like "total".
We can check to see if each letter from each word is within the letters given, and pluck found letters out as we go.
The function below short circuits if a letter is not found.
<?php
function can_form_word_from_letters($word, $letters) {
$letters = str_split($letters);
$word_letters = str_split($word);
foreach($word_letters as $letter) {
$key = array_search($letter, $letters);
if($key === false) return;
unset($letters[$key]); // Letter found, now remove it from letters.
}
return true;
}
$letters = "aabbcdfghmnejaachto";
$words = array('cat','rat','goat','total','egg');
foreach($words as $word) {
$result[$word] = can_form_word_from_letters($word, $letters) ? 'Yes' : 'No';
}
var_dump($result);
Output:
array (size=5)
'cat' => string 'Yes' (length=3)
'rat' => string 'No' (length=2)
'goat' => string 'Yes' (length=3)
'total' => string 'No' (length=2)
'egg' => string 'No' (length=2)

"Unfolding" a String

I have a set of strings, each string has a variable number of segments separated by pipes (|), e.g.:
$string = 'abc|b|ac';
Each segment with more than one char should be expanded into all the possible one char combinations, for 3 segments the following "algorithm" works wonderfully:
$result = array();
$string = explode('|', 'abc|b|ac');
foreach (str_split($string[0]) as $i)
{
foreach (str_split($string[1]) as $j)
{
foreach (str_split($string[2]) as $k)
{
$result[] = implode('|', array($i, $j, $k)); // more...
}
}
}
print_r($result);
Output:
$result = array('a|b|a', 'a|b|c', 'b|b|a', 'b|b|c', 'c|b|a', 'c|b|c');
Obviously, for more than 3 segments the code starts to get extremely messy, since I need to add (and check) more and more inner loops. I tried coming up with a dynamic solution but I can't figure out how to generate the correct combination for all the segments (individually and as a whole). I also looked at some combinatorics source code but I'm unable to combine the different combinations of my segments.
I appreciate if anyone can point me in the right direction.
Recursion to the rescue (you might need to tweak a bit to cover edge cases, but it works):
function explodinator($str) {
$segments = explode('|', $str);
$pieces = array_map('str_split', $segments);
return e_helper($pieces);
}
function e_helper($pieces) {
if (count($pieces) == 1)
return $pieces[0];
$first = array_shift($pieces);
$subs = e_helper($pieces);
foreach($first as $char) {
foreach ($subs as $sub) {
$result[] = $char . '|' . $sub;
}
}
return $result;
}
print_r(explodinator('abc|b|ac'));
Outputs:
Array
(
[0] => a|b|a
[1] => a|b|c
[2] => b|b|a
[3] => b|b|c
[4] => c|b|a
[5] => c|b|c
)
As seen on ideone.
This looks like a job for recursive programming! :P
I first looked at this and thought it was going to be a on-liner (and probably is in perl).
There are other non-recursive ways (enumerate all combinations of indexes into segments then loop through, for example) but I think this is more interesting, and probably 'better'.
$str = explode('|', 'abc|b|ac');
$strlen = count( $str );
$results = array();
function splitAndForeach( $bchar , $oldindex, $tempthread) {
global $strlen, $str, $results;
$temp = $tempthread;
$newindex = $oldindex + 1;
if ( $bchar != '') { array_push($temp, $bchar ); }
if ( $newindex <= $strlen ){
print "starting foreach loop on string '".$str[$newindex-1]."' \n";
foreach(str_split( $str[$newindex - 1] ) as $c) {
print "Going into next depth ($newindex) of recursion on char $c \n";
splitAndForeach( $c , $newindex, $temp);
}
} else {
$found = implode('|', $temp);
print "Array length (max recursion depth) reached, result: $found \n";
array_push( $results, $found );
$temp = $tempthread;
$index = 0;
print "***************** Reset index to 0 *****************\n\n";
}
}
splitAndForeach('', 0, array() );
print "your results: \n";
print_r($results);
You could have two arrays: the alternatives and a current counter.
$alternatives = array(array('a', 'b', 'c'), array('b'), array('a', 'c'));
$counter = array(0, 0, 0);
Then, in a loop, you increment the "last digit" of the counter, and if that is equal to the number of alternatives for that position, you reset that "digit" to zero and increment the "digit" left to it. This works just like counting with decimal numbers.
The string for each step is built by concatenating the $alternatives[$i][$counter[$i]] for each digit.
You are finished when the "first digit" becomes as large as the number of alternatives for that digit.
Example: for the above variables, the counter would get the following values in the steps:
0,0,0
0,0,1
1,0,0 (overflow in the last two digit)
1,0,1
2,0,0 (overflow in the last two digits)
2,0,1
3,0,0 (finished, since the first "digit" has only 3 alternatives)

How to find first non-repetitive character from a string?

I've spent half day trying to figure out this and finally I got working solution.
However, I feel like this can be done in simpler way.
I think this code is not really readable.
Problem: Find first non-repetitive character from a string.
$string = "abbcabz"
In this case, the function should output "c".
The reason I use concatenation instead of $input[index_to_remove] = ''
in order to remove character from a given string
is because if I do that, it actually just leave empty cell so that my
return value $input[0] does not not return the character I want to return.
For instance,
$str = "abc";
$str[0] = '';
echo $str;
This will output "bc"
But actually if I test,
var_dump($str);
it will give me:
string(3) "bc"
Here is my intention:
Given: input
while first char exists in substring of input {
get index_to_remove
input = chars left of index_to_remove . chars right of index_to_remove
if dupe of first char is not found from substring
remove first char from input
}
return first char of input
Code:
function find_first_non_repetitive2($input) {
while(strpos(substr($input, 1), $input[0]) !== false) {
$index_to_remove = strpos(substr($input,1), $input[0]) + 1;
$input = substr($input, 0, $index_to_remove) . substr($input, $index_to_remove + 1);
if(strpos(substr($input, 1), $input[0]) == false) {
$input = substr($input, 1);
}
}
return $input[0];
}
<?php
// In an array mapped character to frequency,
// find the first character with frequency 1.
echo array_search(1, array_count_values(str_split('abbcabz')));
Python:
def first_non_repeating(s):
for i, c in enumerate(s):
if s.find(c, i+1) < 0:
return c
return None
Same in PHP:
function find_first_non_repetitive($s)
{
for($i = 0; i < strlen($s); $i++) {
if (strpos($s, $s[i], $i+1) === FALSE)
return $s[i];
}
}
Pseudocode:
Array N;
For each letter in string
if letter not exists in array N
Add letter to array and set its count to 1
else
go to its position in array and increment its count
End for
for each position in array N
if value at potition == 1
return the letter at position and exit for loop
else
//do nothing (for clarity)
end for
Basically, you find all distinct letters in the string, and for each letter, you associate it with a count of how many of that letter exist in the string. then you return the first one that has a count of 1
The complexity of this method is O(n^2) in the worst case if using arrays. You can use an associative array to increase it's performance.
1- use a sorting algotithm like mergesort (or quicksort has better performance with small inputs)
2- then control repetetive characters
non repetetive characters will be single
repetetvives will fallow each other
Performance : sort + compare
Performance : O(n log n) + O(n) = O(n log n)
For example
$string = "abbcabz"
$string = mergesort ($string)
// $string = "aabbbcz"
Then take first char form string then compare with next one if match repetetive
move to the next different character and compare
first non-matching character is non-repetetive
This can be done in much more readable code using some standard PHP functions:
// Count number of occurrences for every character
$counts = count_chars($string);
// Keep only unique ones (yes, we use this ugly pre-PHP-5.3 syntax here, but I can live with that)
$counts = array_filter($counts, create_function('$n', 'return $n == 1;'));
// Convert to a list, then to a string containing every unique character
$chars = array_map('chr', array_keys($counts));
$chars = implode($chars);
// Get a string starting from the any of the characters found
// This "strpbrk" is probably the most cryptic part of this code
$substring = strlen($chars) ? strpbrk($string, $chars) : '';
// Get the first character from the new string
$char = strlen($substring) ? $substring[0] : '';
// PROFIT!
echo $char;
$str="abbcade";
$checked= array(); // we will store all checked characters in this array, so we do not have to check them again
for($i=0; $i<strlen($str); $i++)
{
$c=0;
if(in_array($str[$i],$checked)) continue;
$checked[]=$str[$i];
for($j=$i+1;$j<=strlen($str);$j++)
{
if($str[$i]==$str[$j])
{
$c=1;
break;
}
}
if($c!=1)
{
echo "First non repetive char is:".$str[$i];
break;
}
}
This should replace your code...
$array = str_split($string);
$array = array_count_values($array);
$array = array_filter($array, create_function('$key,$val', 'return($val == 1);'));
$first_non_repeated_letter = key(array_shift($array));
Edit: spoke too soon. Took out 'array_unique', thought it actually dropped duplicate values. But character order should be preserved to be able to find the first character.
Here's a function in Scala that would do it:
def firstUnique(chars:List[Char]):Option[Char] = chars match {
case Nil => None
case head::tail => {
val filtered = tail filter (_!=head)
if (tail.length == filtered.length) Some(head) else firstUnique(filtered)
}
}
scala> firstUnique("abbcabz".toList)
res5: Option[Char] = Some(c)
And here's the equivalent in Haskell:
firstUnique :: [Char] -> Maybe Char
firstUnique [] = Nothing
firstUnique (head:tail) = let filtered = (filter (/= head) tail) in
if (tail == filtered) then (Just head) else (firstUnique filtered)
*Main> firstUnique "abbcabz"
Just 'c'
You can solve this more generally by abstracting over lists of things that can be compared for equality:
firstUnique :: Eq a => [a] -> Maybe a
Strings are just one such list.
Can be also done using array_key_exists during building an associative array from the string. Each character will be a key and will count the number as value.
$sample = "abbcabz";
$check = [];
for($i=0; $i<strlen($sample); $i++)
{
if(!array_key_exists($sample[$i], $check))
{
$check[$sample[$i]] = 1;
}
else
{
$check[$sample[$i]] += 1;
}
}
echo array_search(1, $check);

Categories