21 iulie 2024

Probleme grafuri & arbori (2)

#5. Aranjarea unor cursuri cu relatie de dependenta intre ele

Se da un numCourses si o lista de dependinte prereq[i] = [a, b], cu semnificatia: pentru a putea participa la cursul a, un student trebuie sa participe intai la cursul b. Sa se gaseasca o aranjare posibila a cursurilor, cu satisfacerea tuturor relatiilor de dependenta. 

Nota: sortare topologica intr-un graf orientat. Se pun intr-o stiva si se elimina succesiv nodurile care nu au arce de iesire, impreuna cu arcele lor de intrare. Se repeta pana cand nu mai ramane niciun nod, apoi se scot nodurile din stiva; ele vor fi sortate topologic. Daca exista un ciclu in graf, nu exista sortare topologica - se va observa ca de la o iteratie la alta nu se mai sterg noduri.

class Solution {
public int[] findOrder(int numCourses, int[][] prereq) {
Map<Integer, Set<Integer>> outgoingEdges = new HashMap<>();
for (int i=0; i<numCourses; i++) {
outgoingEdges.put(i, new HashSet<>());
}
for (int[] edges : prereq) {
int to = edges[0], from = edges[1];
outgoingEdges.get(from).add(to);
}

int result[] = new int[numCourses];
try {
Stack<Integer> stack = topologicalSort(numCourses, outgoingEdges);
int i = 0;
while (!stack.isEmpty()) {
result[i++] = stack.pop();
}
} catch (CircularException e) {
return new int[0];
}
return result;
}

private Stack<Integer> topologicalSort(int numCourses, Map<Integer, Set<Integer>> outgoingEdges) {
Stack<Integer> nodeStack = new Stack<>();
int remainingNodes = outgoingEdges.size();
while (!outgoingEdges.isEmpty()) {
List<Integer> connectionsToDelete = new LinkedList<>();
for (int i=0; i<numCourses; i++) {
if (outgoingEdges.containsKey(i) && outgoingEdges.get(i).isEmpty()) {
nodeStack.push(i);
outgoingEdges.remove(i);
connectionsToDelete.add(i);
}
}
outgoingEdges.forEach((k,v) -> v.removeAll(connectionsToDelete));
connectionsToDelete.clear();
// este un ciclu atunci cand dupa o parcurgere completa nu se mai scot noduri
if (remainingNodes == outgoingEdges.size()) {
throw new CircularException();
}
remainingNodes = outgoingEdges.size();
}
return nodeStack;
}

private class CircularException extends RuntimeException {
public CircularException() {
super();
}
}
}

Varianta mai eficienta: sortare topologica folosind DSF. Dupa obtinerea unei ordini topologice, trebuie sa se verifice ca graful nu contine cicluri.

class Solution {
public int[] findOrder(int numCourses, int[][] prereq) {
List<List<Integer>> adjList = getAdjList(numCourses, prereq);
boolean visited[] = new boolean[numCourses]; // all false
Stack<Integer> stack = new Stack<>();
int result[] = new int[numCourses];

for (int i=0; i<numCourses; i++) {
if (!visited[i]) {
dfs(i, adjList, stack, visited);
}
}
int i = 0;
while (!stack.isEmpty()) {
result[i++] = stack.pop();
}

// check for any cycle
for (int node = 0; node < numCourses; node++) {
for (int conn : adjList.get(node)) {
// we have a vertex from node -> conn
// if conn appears before node in the ordering => cycle
if (indexOf(conn, result) < indexOf(node, result)) {
return new int[0]; // cycle
}
}
}

return result;
}

private void dfs(int node, List<List<Integer>> adjList, Stack<Integer> stack, boolean visited[]) {
visited[node] = true;
for (int neighbor : adjList.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor, adjList, stack, visited);
}
}
stack.push(node);
}

private List<List<Integer>> getAdjList(int numCourses, int[][] prereq) {
List<List<Integer>> adjList = new ArrayList<>();
for (int i=0; i<numCourses; i++) {
adjList.add(new ArrayList<>());
}
for (int[] edges : prereq) {
int to = edges[0], from = edges[1];
adjList.get(from).add(to);
}
return adjList;
}

private int indexOf(int node, int[] order) {
for (int i=0; i<order.length; i++) {
if (order[i] == node) {
return i;
}
}
return -1;
}
}

#6. Primul stramos comun a doua noduri dintr-un arbore binar

Cand avem acces la parintii unui nod:

class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p == root || q == root) {
return root;
}

int diffDepth = depth(root, p) - depth(root, q);
while (diffDepth > 0) {
p = p.parent; // bring p on the same level with q
diffDepth--;
}
while (diffDepth < 0) {
q = q.parent; // bring q on the same level with p
diffDepth++;
}

while (p != q && p != null && q != null) {
// go up together
p = q.parent;
p = q.parent;
}

return (p != null && q != null) ? p : null;
}

private int depth(TreeNode root, TreeNode node) {
int depth = 0;
while (node != root) {
depth++;
node = node.parent;
}
return depth;
}
}

Cand nu avem acces la parintii unui nod:

class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p == root || q == root || root == null) {
return root;
}
return search(root, p, q);
}

private TreeNode search(TreeNode node, TreeNode p, TreeNode q) {
if (node == p || node == q) {
return node;
}
boolean isPinLeft = isNodeInSubtree(node.left, p);
boolean isQinLeft = isNodeInSubtree(node.left, q);
if (isPinLeft != isQinLeft) { // if p and q are in different branches of node
return node; // common ancestor
} else {
TreeNode nodeNext = isPinLeft ? node.left : node.right;
return search(nodeNext, p, q);
}
}

private boolean isNodeInSubtree(TreeNode subtree, TreeNode node) {
if (subtree == null) {
return false;
}
if (subtree.val == node.val) {
return true;
}
return isNodeInSubtree(subtree.left, node) || isNodeInSubtree(subtree.right, node);
}
// issue: repetitive calls to isNodeInSubtree when it already found the node
}

Alternativa - fara acces la parinti, tinem minte calea pana la nod in format L/R

class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p == root || q == root || root == null) {
return root;
}
search(root, p, "", q, "");
TreeNode ancestor = root;
int i = 0;
while (finalPathP.length() > i && finalPathQ.length() > i && finalPathP.charAt(i) == finalPathQ.charAt(i)) {
char c = finalPathP.charAt(i);
ancestor = c == 'L' ? ancestor.left : ancestor.right;
i++;
}
return ancestor;
}

String finalPathP = null;
String finalPathQ = null;

private void search(TreeNode node, TreeNode p, String pathP, TreeNode q, String pathQ) {
if (node == null) {
return;
}
if (node.val == p.val) {
finalPathP = pathP;
}
if (node.val == q.val) {
finalPathQ = pathQ;
}
if (finalPathP == null || finalPathQ == null) {
search(node.left, p, pathP + "L", q, pathQ + "L");
search(node.right, p, pathP + "R", q, pathQ + "R");
}
}
}

19 iulie 2024

Probleme grafuri & arbori (1)

#1. Aflare daca exista o cale intre doua noduri dintr-un graf

Graf neorientat cu n noduri pentru care se da o lista de muchii edges[i] = [n1, n2] care inseamna ca exista o legatura directa intre n1 si n2. Sa se afle daca din n1 se poate ajunge in n2.

Nota: BFS. Nu este necesar sa folosim Map, un simplu ArrayList e suficient.

class Solution {
public boolean validPath(int n, int[][] edges, int source, int destination) {
if (source == destination) {
return true;
}

Map<Integer, Node> nodes = new HashMap<>();
for (int i=0; i<n; i++) {
nodes.put(i, new Node(i));
}

for (int i=0; i<edges.length; i++) {
int from = edges[i][0], to = edges[i][1];
nodes.get(from).addNeighbor(nodes.get(to));
nodes.get(to).addNeighbor(nodes.get(from));
}

Node startNode = nodes.get(source);
if (startNode.isLinkedTo(nodes.get(destination))) {
return true;
}
startNode.setVisited();

Queue<Node> queue = new LinkedList<>();
queue.addAll(startNode.neighbors);
while(!queue.isEmpty()) {
Node next = queue.remove();
if (!next.visited) {
next.setVisited();
if (next.isLinkedTo(nodes.get(destination))) {
return true;
}
queue.addAll(next.neighbors);
}
}

return false;
}

private class Node {
int value;
List<Node> neighbors;
boolean visited = false;

public Node (int value) {
this.value = value;
this.neighbors = new ArrayList<>();
}

public void addNeighbor(Node node) {
this.neighbors.add(node);
}

public boolean isLinkedTo(Node node) {
return this.neighbors.contains(node);
}

public void setVisited() {
this.visited = true;
}
}
}

#2. Transformarea unui array sortat intr-un BST (binary search tree)

Sirul este sortat crescator. In arborele creat, toti descendentii din stanga ai unui nod <= valoarea nodului, iar descendentii din dreapta > valoarea nodului. In plus, arborele trebuie sa fie aiba inaltimea minima posibila.

class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return transform(nums, 0, nums.length - 1);
}

TreeNode transform(int[] nums, int indexFrom, int indexTo) {
if (indexFrom == indexTo) {
return new TreeNode(nums[indexFrom]);
}
if (indexFrom == indexTo - 1) {
TreeNode root = new TreeNode(nums[indexTo]);
root.left = new TreeNode(nums[indexFrom]);
return root;
}

int size = indexTo - indexFrom + 1;
TreeNode root = new TreeNode(nums[indexFrom + size/2]);
root.left = transform(nums, indexFrom, indexFrom + size/2 - 1);
root.right = transform(nums, indexFrom + size/2 + 1, indexTo);
return root;
}
}

#3. Arbore binar echilibrat

Dandu-se un arbore binar, sa se afle daca este echilibrat, adica pt fiecare nod diferenta intre inaltimile in jos ale fiecarui subarbore sa nu fie mai mare decat 1.

class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
try {
findHeightUnderNode(root);
} catch (UnbalancedTreeException e) {
return false;
}
return true;
}

private int findHeightUnderNode(TreeNode node) throws UnbalancedTreeException {
if (node.left == null && node.right == null) {
return 1;
}
int heightUnderLeft = node.left == null ? 0 : findHeightUnderNode(node.left);
int heightUnderRight = node.right == null ? 0 : findHeightUnderNode(node.right);
if (Math.abs(heightUnderLeft - heightUnderRight) > 1) {
throw new UnbalancedTreeException();
}
return 1 + Math.max(heightUnderLeft, heightUnderRight);
}

private class UnbalancedTreeException extends RuntimeException {
public UnbalancedTreeException() {
super();
}
}
}

#4. Validarea unui BST

Se da un arbore binar. Sa se verifice daca este un arbore binar de cautare (BST).

class Solution {
public boolean isValidBST(TreeNode root) {
try {
checkValid(root, null, null);
} catch(InvalidBSTException e) {
return false;
}
return true;
}

private void checkValid(TreeNode node, Integer lessThan, Integer moreThan) {
// lessThan, moreThan - constrangeri propagate de sus in jos
if (node == null || (node.left == null && node.right == null)) {
return;
}
if (node.left != null) {
if (node.left.val >= node.val || (lessThan != null && node.left.val >= lessThan) ||
(moreThan != null && node.left.val <= moreThan)) {
throw new InvalidBSTException();
}
}
if (node.right != null) {
if (node.right.val <= node.val || (lessThan != null && node.right.val >= lessThan) ||
(moreThan != null && node.right.val <= moreThan)) {
throw new InvalidBSTException();
}
}

checkValid(node.left, min(node.val, lessThan), moreThan);
checkValid(node.right, lessThan, max(node.val, moreThan));
}

private int min(Integer x, Integer y) {
if (x != null && y != null) {
return Math.min(x,y);
}
return x != null ? x : y;
}

private int max(Integer x, Integer y) {
if (x != null && y != null) {
return Math.max(x,y);
}
return x != null ? x : y;
}

private class InvalidBSTException extends RuntimeException {
public InvalidBSTException() {
super();
}
}
}

17 iulie 2024

Probleme programare dinamică & recursivitate (2)

#5. Generarea tuturor parantezelor 

Dându-se un număr n, să se genereze toate combinațiile de paranteze bine-formate. Adică: fiecare paranteză deschisă formează un cuplu cu o paranteză închisă și cuplul începe întotdeauna cu paranteza deschisă.

Exemplu: Input: n = 3

Output: ["((()))","(()())","(())()","()(())","()()()"]

class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
// all strings will have 2*n length and are built from scratch
generateParenthesis(2*n, n, "", result);
return result;
}

private void generateParenthesis(int n, int initialN, String generated, List<String> result) {
if (n == 1) {
// in the last call we can only use )
if (isValid(generated + ')', true, initialN)) {
result.add(generated + ')');
}
return;
}

// recurse only if we are on the good path with the partial result
if (isValid(generated + '(', false, initialN)) {
generateParenthesis(n-1, initialN, generated + '(', result);
}
if (isValid(generated + ')', false, initialN)) {
generateParenthesis(n-1, initialN, generated + ')', result);
}
}

private boolean isValid(String str, boolean checkFinal, int n) {
int left = 0, right = 0;
for (char c : str.toCharArray()) {
if (c == '(') {
left++;
} else {
right++;
if (right > left) {
return false;
}
}
}
return checkFinal ?
left == right : // a well-formed string has the same number of ( and )
left <= n; // a partial result should not have ( count more than n
}
}

#6. Asezarea unor cutii intr-o stiva

Avem N cutii, fiecare cutie fiind reprezentata in 3 dimensiuni (width, length, height). Ele se pot stivui astfel incat cutia aflata deasupra altei cutii va avea w2 <= w1, l2 <= l1 si h2 <= h1. Nu toate cutiile se pot stivui, dar ele se pot roti astfel incat o inaltime sa devina lungime etc. Care e inaltimea celei mai inalte stive posibile?

Nota: la prima vedere, problema este o variatie la problema Maximum subarray (aflarea sumei maxime a unui subsir dintr-un sir), la care se adauga cateva constrangeri. Am implementat urmatorii pasi:

  • transformarea obiectelor-cutie prin rotire astfel incat inaltimea sa fie dimensiunea cu valoarea cea mai mare dintre cele 3D  (greedy)
  • sortarea cutiilor dupa inaltime, urmata de celelalte doua dimensiuni, descrescator (*)
  • aplicarea algoritmului pt Max subarray mentionat anterior, cu adaptarea constrangerilor pentru cutii (programare dinamica)

(*) neasteptat de tricky este sortarea cutiilor. Nu toate cutiile sunt ordonabile, iar un algoritm eficient precum Mergesort greseste pentru anumite cazuri. Asa ca am optat pentru un Bubblesort cu numar maxim de comparatii intre elemente, pentru a forta o relatie de ordonare intre oricare 2 elemente din sirul de cutii. (S-a dovedit si ca doar Bubblesort-ul cu 2 for-uri va functiona corect pentru toate testele!)

Nota: nu e extrem de eficient, dar e corect. Complexitate: timp O(N^2) determinata in principal de Bubblesort, spatiu O(N) pt un array suplimentar.

class Solution {
public int maxHeight(int[][] boxes) {
// boxes[i] = (w, len, h)

rotateObjectsMaxHeight(boxes);

if (boxes.length == 1) {
return boxes[0][2];
}

sortByMaxHeightAndWLDesc(boxes);

int maxSumHeight = boxes[0][2];
int largest[] = new int[boxes.length];
largest[0] = boxes[0][2];
int lastAddedIndex = 0; // index of the last added box to obtain largest so far

for (int i=1; i<boxes.length; i++) {
if (canPlace(boxes[lastAddedIndex], boxes[i])) {
largest[i] = largest[lastAddedIndex] + boxes[i][2];
} else {
largest[i] = boxes[i][2];
// try to put current box on any previous box if it gives profit
for (int j=0; j<i; j++) {
if (j != lastAddedIndex) {
if (canPlace(boxes[j], boxes[i]) && largest[i] < largest[j] + boxes[i][2]) {
largest[i] = largest[j] + boxes[i][2];
}
}
}
}

if (maxSumHeight < largest[i]) {
maxSumHeight = largest[i];
lastAddedIndex = i;
}
}

return maxSumHeight;
}

private void rotateObjectsMaxHeight(int[][] boxes) {
// h = max(w, len, h) => swap h with the max
for (int i=0; i<boxes.length; i++) {
int max = Math.max(boxes[i][0], Math.max(boxes[i][1], boxes[i][2]));
if (max != boxes[i][2]) {
int aux = boxes[i][2]; // h
boxes[i][2] = max;
if (max == boxes[i][0]) { // w
boxes[i][0] = aux;
} else if (max == boxes[i][1]) { // l
boxes[i][1] = aux;
}
}
}
}

private boolean canPlace(int[] boxUnder, int[] boxTop) {
boolean firstCheck = boxUnder[0] >= boxTop[0] && boxUnder[1] >= boxTop[1];
boolean rotateCheck = boxUnder[0] >= boxTop[1] && boxUnder[1] >= boxTop[0];
return firstCheck || rotateCheck;
}

private boolean equals(int[] boxUnder, int[] boxTop) {
boolean firstCheck = boxUnder[0] == boxTop[0] && boxUnder[1] == boxTop[1];
boolean rotateCheck = boxUnder[0] == boxTop[1] && boxUnder[1] == boxTop[0];
return firstCheck || rotateCheck;
}

public void sortByMaxHeightAndWLDesc(int[][] boxes) {
// max complexity bubble sort needed because we need to make as many comparisons as possible
for (int i=0; i<boxes.length - 1; i++) {
for (int j=i+1; j<boxes.length; j++) {
boolean rightHeightHigher = boxes[i][2] < boxes[j][2];
boolean equalHeightsWithRightBigger = boxes[i][2] == boxes[j][2] &&
canPlace(boxes[j], boxes[i]) &&
!equals(boxes[j], boxes[i]);
if (rightHeightHigher || equalHeightsWithRightBigger) {
int[] aux = boxes[i];
boxes[i] = boxes[j];
boxes[j] = aux;
}
}
}
}
}

#7. In cate moduri se pot pune paranteze unei expresii booleene

Se da o expresie formata din valorile 0, 1 (false, true) si operatorii binari &, | si ^. In cate moduri se pot pune paranteze astfel incat evaluarea expresiei sa fie result? Parantezele de genul (( )) nu sunt valide.

Exemplu: countEval('1 & 0 | 1', true) = 2, posibilitatile sunt 1 & (0 | 1) si (1 & 0) | 1.

Rezolvare: cateva observatii:

  1. cazuri de baza: countEval(Strings.EMPTY, result) = 0 si countEval(str, result) ~ str.equals(result) adica 0 sau 1, cand str are un singur caracter.
  2. valorile 0 si 1 vor sta intotdeauna pe pozitiile 0, 2, 4, ... n , pe cand operatorii binari vor sta intotdeauna pe pozitiile 1, 3, 5, ... n-1
    • asta duce la un for (int i=1; i<str.length(); i+=2) in care sa calculam subproblemele
  3. daca ne situam pe pozitia unui operator (i), putem calcula recursiv leftCountTrue = countEval(str.substring(0, i), true) si rightCountTrue = countEval(str.substring(i+1), true). Similar si leftCountFalse, rightCountFalse. Toate variabilele ne trebuie pentru ca la un '|', de exemplu, evaluarea uneia din parti poate fi si false, iar rezultatul final sa fie true.
  4. se calculeaza totalCount = toate modurile posibile de a obtine orice rezultat al evaluarii, care consta din toate modurile de a aranja parantezele in partea stanga * toate modurile de a aranja parantezele in partea dreapta
    • totalCount = (leftTrue + leftFalse) * (rightTrue + rightFalse);
  5. calculam totalCountTrue in functie de operatorul de pe pozitia i:
    • '&' --> totalCountTrue = leftCountTrue * rightCountTrue
    • '|'  --> totalCountTrue = leftCountTrue * rightCountTrue + leftCountTrue * rightCountFalse + leftCountFalse * rightCountTrue;
    • '^' --> totalCountTrue = leftCountTrue * rightCountFalse + leftCountFalse * rightCountTrue;
  6. in functie de rezultatul dorit, solutia subproblemei curente este totalCountTrue sau totalCount - totalCountTrue. Ea se adauga la variabila pentru pastrarea tuturor modurilor.
Optimizare: unele apeluri ale metodei countEval se repeta. Putem pastra un tabel unde stocam rezultatul apelului curent pe care sa-l obtinem rapid, daca exista, inainte de intrarea in for.
  • if (memo.containsKey(result + str)) return memo.get(result+str);
  • memo.put(result+str, countWays);

16 iulie 2024

Probleme programare dinamică & recursivitate (1)

#1. Robot in a Grid (p.d.)

Un robot se află într-o matrice, in coltul din stanga sus. El se poate deplasa doar la dreapta sau in jos, daca celula vizata nu contine un obstacol. Tinta lui este sa ajunga in coltul din dreapta jos al matricii. Sa se afle in cate moduri poate ajunge robotul la destinatie.

class Solution {
public int uniquePathsWithObstacles(int[][] grid) {
// go from destination to origin
Map<Integer,Integer> cache = new HashMap<>();
return search(grid, grid.length-1, grid[0].length-1, cache);
}

private int search(int[][] grid, int i, int j, Map<Integer,Integer> cache) {
if (reachedBack(grid, i, j)) {
return 1; // found one way!
}

// Number of ways to reach grid[i][j] = ways to reach grid[i-1][j] (go up) +
// ways to reach grid[i][j-1] (left)

if (isCellFit(grid, i, j)) {
int indexUp = (i-1) * grid[0].length + j;
int pathUp = 0;
// element indexUp could be already visited but not necessarily attainable from grid[i][j]
if (isCellFit(grid, i-1, j)) {
pathUp = cache.containsKey(indexUp) ? cache.get(indexUp) : search(grid, i-1, j, cache);
}

int indexLeft = i * grid[0].length + j-1;
int pathLeft = 0;
if (isCellFit(grid, i, j-1)) {
pathLeft = cache.containsKey(indexLeft) ? cache.get(indexLeft) : search(grid, i, j-1, cache);
}
int index = i * grid[0].length + j;
cache.put(index, pathUp + pathLeft); // grid[i][j] is visited and its result established

return pathUp + pathLeft;
}

return 0;
}

private boolean isCellFit(int[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
return false; // out of grid
}
return grid[i][j] == 0; // should be obstacle free
}

private boolean reachedBack(int[][] grid, int i, int j) {
return i==0 && j==0 && grid[i][j] == 0;
}
}

#2. Toate submultimile unei multimi (recursivitate)

class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
result.add(new ArrayList<>());
result.addAll(find(Arrays.stream(nums).boxed().collect(Collectors.toList())));
return result;
}

private List<List<Integer>> find(List<Integer> set) {
if (set.size() == 1) {
return Arrays.asList(Arrays.asList(set.get(0)));
}
List<List<Integer>> result = new ArrayList<>();
result.add(Arrays.asList(set.get(0))); // primul element
List<List<Integer>> subsets = find(set.subList(1, set.size()));
result.addAll(subsets); // submultimile din (A - primul elem)
for (List<Integer> subset : subsets) {
List<Integer> subsetCopy = clone(subset);
// adaug primul element la toate submultimile din (A - primul elem)
subsetCopy.add(set.get(0));
result.add(subsetCopy);
}
return result;
}

private List<Integer> clone(List<Integer> set) {
List<Integer> clonedSet = new ArrayList<>();
clonedSet.addAll(set);
return clonedSet;
}
}

#3. Inmultirea a doua numere fara a folosi * , doar prin adunari, scaderi sau deplasari de biti, in numar minim de operatii. (recursivitate)

public class MultiplyIntegers {
public static int solve(int a, int b) {
int minAb = Math.min(a, b);
int maxAb = Math.max(a, b);
return multiply(minAb, maxAb); // to optimize the number of recursions
}

private static int multiply(int a, int b) {
if (a == 0 || b == 0) {
return 0;
}
if (a == 1) {
return b;
}

// a is even => multiply(a,b) = 2 * multiply(a/2, b)
// a is odd => multiply(a,b) = 2 * multiply(a/2, b) + b

int partialRes = multiply(a >> 1, b); // multiply (a/2, b)
partialRes <<= 1; // partialRes *= 2
if (a % 2 == 1) {
partialRes += b;
}
return partialRes;
}

public void test() {
List<TestCase> testCases = new ArrayList<>();
testCases.add(new TestCase(5, 3, 15));
testCases.add(new TestCase(10, 7, 70));
testCases.add(new TestCase(31, 35, 1085));
testCases.add(new TestCase(16, 6, 96));
testCases.add(new TestCase(3, 0, 0));
testCases.add(new TestCase(27, 4, 108));
testCases.add(new TestCase(36475, 4249, 154982275));
testCases.add(new TestCase(4585, 42339, 194124315));

long startTime = System.nanoTime();
for (TestCase testCase : testCases) {
int result = MultiplyIntegers.solve(testCase.a, testCase.b);
System.out.println("Test case #" + testCases.indexOf(testCase) + " " +
(result == testCase.result ?
"PASSED" :
"FAILED with output=" + result + " " +
"[expected=" + testCase.result + "] \t\t<------"));
}
long endTime = System.nanoTime();
System.out.println("Executed in " + (endTime - startTime)/1000000 + " ms.");
}

public static void main(String args[]) {
new MultiplyIntegers().test();
}

public class TestCase {
public final int a;
public final int b;
public final int result;
public TestCase(int a, int b, int result) {
this.a = a;
this.b = b;
this.result = result;
}
}
}

#4. Turnurile din Hanoi

Se dau 3 turnuri: A, B, C. Pe fiecare turn se pot aseza discuri aranjate descrescator, cele mai mari stand la baza si cele mai mici deasupra. Sa se mute toate discurile din A in C, folosind B ca intermediar, respectand conditia. Care e numarul minim de miscari pentru a realiza acest transfer?

Nota: se porneste de la cazul de baza - 1 disc, care are nevoie de o singura miscare pt a ajunge in C. Apoi simulam pe hartie ce se intampla cand avem 2 discuri. Apoi 3. De fiecare data se poate observa (de la n >= 2) ca turnul B se comporta ca un buffer. De asemenea, pentru a rezolva cazuri superioare, ne putem folosi de rezultatul anterior deja calculat, la care mai avem de mutat un singur disc (ultimul). Astfel: pentru a muta n discuri, trebuie sa mutam n-1 discuri din A in B (folosind C ca intermediar) pentru a lasa liber ultimul disc din A, apoi mutat direct discul din A in C, apoi mutate cele n-1 discuri din B in C (folosind A ca intermediar). Se observa ca numarul de mutari este 2^n - 1.

Nota: design-ul solutiei. Obiectele Disk si Tower sunt "personalizate". Am facut cateva validari. Tower detine o stiva de obiecte Disk. Adaugarea se face intr-o metoda din Tower afectand un alt obiect Tower din care se scoate un disc.

import java.util.*;

public class Hanoi {
private int moveDisks (int n, Tower origin, Tower destination, Tower buffer) throws Exception {
if (n == 1) {
destination.takeFrom(origin);
return 1; // one move
}

int steps = 0;
steps += moveDisks(n-1, origin, buffer, destination); // move first n-1 from origin to buffer
destination.takeFrom(origin); // move last element from origin to dest
steps++;
steps += moveDisks(n-1, buffer, destination, origin); // move back n-1 elements from buffer to dest

return steps;
}

public static void main(String args[]) {
Hanoi hanoi = new Hanoi();
hanoi.test();
}

public void test() {
Hanoi hanoi = new Hanoi();

List<Disk> disks = new ArrayList<>();
disks.add(new Disk(1, 5));
disks.add(new Disk(2, 2));
disks.add(new Disk(3, 8));
disks.add(new Disk(4, 1));
disks.add(new Disk(5, 3));
disks.add(new Disk(6, 4));
disks.add(new Disk(6, 7));
disks.add(new Disk(6, 6));

// hanoi(n) = 2^n - 1

List<TestCase> testCases = new ArrayList<>();
testCases.add(new TestCase(new Tower("A", disks.subList(0, 1)), new Tower("B"), new Tower("C"), 1));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 2)), new Tower("B"), new Tower("C"), 3));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 3)), new Tower("B"), new Tower("C"), 7));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 4)), new Tower("B"), new Tower("C"), 15));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 5)), new Tower("B"), new Tower("C"), 31));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 6)), new Tower("B"), new Tower("C"), 63));
testCases.add(new TestCase(new Tower("A", disks.subList(0, 7)), new Tower("B"), new Tower("C"), 127));
testCases.add(new TestCase(new Tower("A", disks), new Tower("B"), new Tower("C"), 255));
for (TestCase tc : testCases) {
try {
int numberOfDisks = tc.origin.getNumberOfDisks();
int res = hanoi.moveDisks(numberOfDisks, tc.origin, tc.destination, tc.buffer);
boolean testResult = res == tc.expectedMoves && tc.origin.getNumberOfDisks() == 0 &&
tc.buffer.getNumberOfDisks() == 0 &&
tc.destination.getNumberOfDisks() == numberOfDisks;
System.out.println("Test case #" + testCases.indexOf(tc) + " " +
(testResult ?
"PASSED" :
"FAILED with moves=" + res + " " +
"[expected.moves=" + tc.expectedMoves + "] AND " +
"with origin.size=" + tc.origin.getNumberOfDisks() + " AND " +
"with buffer.size=" + tc.buffer.getNumberOfDisks() + " AND " +
"with destination.size=" + tc.destination.getNumberOfDisks() +
" [expected.dest.size=" + numberOfDisks + "] \t\t<------"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

class Tower {
String label;
Stack<Disk> disks;

public Tower(String label) {
this.label = label;
this.disks = new Stack<>();
}

public Tower(String label, List<Disk> initDisks) {
this.label = label;
this.disks = new Stack<>();
Collections.sort(initDisks);
for (Disk disk : initDisks) {
this.disks.push(disk);
}
}

public void takeFrom(Tower from) throws Exception {
if (from.disks.isEmpty()) {
throw new Exception("Nothing to add from!");
}
Disk diskToAdd = from.disks.peek();
if (!disks.isEmpty() && diskToAdd.value > disks.peek().value) {
throw new Exception("Cannot add disk with value " + diskToAdd.value + " to tower " + label);
}
disks.push(from.disks.pop());
}

public int getNumberOfDisks() {
return this.disks.size();
}

@Override
public String toString() {
return "Tower " + label + " has " + disks.size() + " disks.";
}
}

class Disk implements Comparable<Disk> {
int label;
int value;

public Disk(int label, int value) {
this.label = label;
this.value = value;
}

@Override
public int compareTo(Disk disk) {
if (disk.value == value) {
return 0;
}
return disk.value - value;
}
}

public class TestCase {
public final Tower origin;
public final Tower buffer;
public final Tower destination;
public final int expectedMoves;
public TestCase(Tower tower, Tower buffer, Tower destination, int expectedMoves) {
this.origin = tower;
this.buffer = buffer;
this.destination = destination;
this.expectedMoves = expectedMoves;
}
}
}