From Codechef:
A string is considered balanced if and only if all the characters occur in it equal number of times.
You are given a strin
Following code implement the solution, in Java, with unit test.
The algorithm is almost identical as in @ruakh's answer, if not identical.
BalanceString.java
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Assume string only contains A-Z, the 26 uppercase letter,
* given a string, you can replace a char with another char from the 26 letter,
*
figure out the minimum replacement required to make the string balance,
*
which means each char in the string occurs the same time,
*
* @author eric
* @date 2/2/19 8:54 PM
*/
public class BalanceString {
private final char minChar;
private final char maxChar;
private final int distinctChars; // total distinct char count,
public static final BalanceString EN_UPPER_INSTANCE = new BalanceString('A', 'Z');
public BalanceString(char minChar, char maxChar) {
this.minChar = minChar;
this.maxChar = maxChar;
this.distinctChars = maxChar - minChar + 1;
if (distinctChars <= 0)
throw new IllegalArgumentException("invalid range of chars: [" + minChar + ", " + maxChar + "]");
}
/**
* Check minimal moves needed to make string balanced.
*
* @param str
* @return count of moves
*/
public int balanceCount(String str) {
// System.out.printf("string to balance:\t%s\n", str);
int len = str.length(); // get length,
if (len <= 2) return 0; // corner cases,
Map coMap = figureOccurs(str); // figure occurrences,
Integer[] occurs = sortOccursReversely(coMap); // reversely order occurrences,
int m = coMap.size(); // distinct char count,
int maxN = (len < distinctChars ? len : distinctChars); // get max possible distinct char count, included,
int smallestMoves = Integer.MAX_VALUE; // smallest moves, among all possible n,
// check each possible n, and get its moves,
for (int n = 1; n <= maxN; n++) {
if (len % n == 0) {
int moves = figureMoves(len, coMap, occurs, m, n);
if (moves < smallestMoves) smallestMoves = moves;
}
}
return smallestMoves;
}
/**
* Figure occurs for each char.
*
* @param str
* @return
*/
protected Map figureOccurs(String str) {
Map coMap = new HashMap<>();
for (char c : str.toCharArray()) {
if (c < minChar || c > maxChar)
throw new IllegalArgumentException(c + " is not within range 'A-Z'");
if (!coMap.containsKey(c)) coMap.put(c, 1);
else coMap.put(c, coMap.get(c) + 1);
}
return coMap;
}
/**
* Get reverse sorted occurrences.
*
* @param coMap
* @return
*/
protected Integer[] sortOccursReversely(Map coMap) {
Integer[] occurs = new Integer[coMap.size()];
coMap.values().toArray(occurs);
Arrays.sort(occurs, Collections.reverseOrder());
return occurs;
}
/**
* Figure moves needed to balance.
*
* @param len length of string,
* @param coMap
* @param m original distinct elements count,
* @param n new distinct elements count,
* @return count of moves,
*/
protected int figureMoves(int len, Map coMap, Integer[] occurs, int m, int n) {
int avgOccur = len / n; // average occurrence,
int moves = 0;
if (n == m) { // distinct chars don't change,
for (Integer occur : occurs) {
if (occur <= avgOccur) break;
moves += (occur - avgOccur);
}
} else if (n < m) { // distinct chars decrease,
for (int i = 0; i < n; i++) moves += Math.abs(occurs[i] - avgOccur); // for elements kept,
for (int i = n; i < m; i++) moves += occurs[i]; // for elements to replace,
moves >>= 1;
} else { // distinct chars increase,
for (int i = 0; i < occurs.length; i++) moves += Math.abs(occurs[i] - avgOccur); // for existing elements,
moves += ((n - m) * avgOccur); // for new elements,
moves >>= 1;
}
return moves;
}
public char getMinChar() {
return minChar;
}
public char getMaxChar() {
return maxChar;
}
public int getDistinctChars() {
return distinctChars;
}
}
BalanceStringTest.java
(Unit test, via TestNG)
import org.testng.Assert;
import org.testng.annotations.Test;
/**
* BalanceString test.
*
* @author eric
* @date 2/2/19 9:36 PM
*/
public class BalanceStringTest {
private BalanceString bs = BalanceString.EN_UPPER_INSTANCE;
@Test
public void test() {
// n < m case,
Assert.assertEquals(bs.balanceCount("AAAABBBC"), 1); // e.g 1A -> B,
Assert.assertEquals(bs.balanceCount("AAAAABBC"), 2); // e.g 1A -> B, 1C -> B,
Assert.assertEquals(bs.balanceCount("AAAAAABC"), 2); // e.g 1B -> A, 1C -> A,
Assert.assertEquals(bs.balanceCount("AAAAAAAB"), 1); // e.g 1B -> A,
// n > m case,
Assert.assertEquals(bs.balanceCount("AAAABBBBCCCCDDDDEEEEAAAA"), 4); // add 1 new char, e.g change 4 A to 4 F,
Assert.assertEquals(bs.balanceCount(genIncSeq(10)), 15); // A-J, 10 distinct chars, 55 in length; solved by add 1 new char, need 15 steps,
// n == m case,
Assert.assertEquals(bs.balanceCount(genIncSeq(3)), 1); // A-C, 3 distinct chars, 6 in length; symmetric, solved with same distinct chars, need 1 steps,
Assert.assertEquals(bs.balanceCount(genIncSeq(11)), 15); // A-K, 11 distinct chars, 66 in length; symmetric, solved with same distinct chars, need 15 steps,
// n < m, or n > m case,
Assert.assertEquals(bs.balanceCount("ABAC"), 1); // e.g 1A -> B, or 1A -> D,
}
// corner case,
@Test
public void testCorner() {
// m <= 2,
Assert.assertEquals(bs.balanceCount(""), 0);
Assert.assertEquals(bs.balanceCount("A"), 0);
Assert.assertEquals(bs.balanceCount("AB"), 0);
Assert.assertEquals(bs.balanceCount("AA"), 0);
/*------ m == n == distinctChars ------*/
String mndBalanced = genMndBalancedSeq(); // each possible char occurs exactly once, already balanced,
Assert.assertEquals(mndBalanced.length(), bs.getDistinctChars());
Assert.assertEquals(bs.balanceCount(mndBalanced), 0); // no need change,
char lastChar = mndBalanced.charAt(mndBalanced.length() - 1);
String mndOneDup = mndBalanced.replace(lastChar, (char) (lastChar - 1)); // (distinctChars -2) chars occur exactly once, one occurs twice, one is missing, thus it's one step away to balance,
Assert.assertEquals(mndOneDup.length(), bs.getDistinctChars());
Assert.assertEquals(bs.balanceCount(mndOneDup), 1); // just replace the duplicate char with missing char,
}
// invalid input,
@Test(expectedExceptions = IllegalArgumentException.class)
public void testInvalidInput() {
Assert.assertEquals(bs.balanceCount("ABAc"), 1);
}
// invalid char range, for constructor,
@Test(expectedExceptions = IllegalArgumentException.class)
public void testInvalidRange() {
new BalanceString('z', 'a');
}
/**
* Generate a string, with first char occur once, second twice, third three times, and so on.
* e.g A, ABB, ABBCCC, ABBCCCDDDD,
*
* @param m distinct char count,
* @return
*/
private String genIncSeq(int m) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < m; i++) {
for (int j = 0; j <= i; j++) sb.append((char) (bs.getMinChar() + i));
}
return sb.toString();
}
/**
* Generate a string that contains each possible char exactly once.
*
For [A-Z], it could be: "ABCDEFGHIJKLMNOPQRSTUVWXYZ".
*
* @return
*/
private String genMndBalancedSeq() {
StringBuilder sb = new StringBuilder();
char minChar = bs.getMinChar();
int distinctChars = bs.getDistinctChars();
for (int i = 0; i < distinctChars; i++) {
sb.append((char) (minChar + i));
}
return sb.toString();
}
}
All test cases would pass.
O(len) + O(m * lg(m)) + O(m * factorCount)
O(len), there are several sequential loop.O(m*lg(m)), which is at most O(distinctChars * lg(distinctChars)), thus constant, and only sort once.O(m).minChar, maxChar].O(len)
O(len).O(m).O(m).Where:
len is string length.m is distinct char count in original stringdistinctChars is distinct char count, e.g 26.maxN max possible distinct char count, included,factorCount divisible number count in range [1, n], by len,minChar min char, e.g AmaxChar max char, e.g ZAnd:
len >= mm <= distinctChars