The Optimal Binary Search Tree (OBST) is a classic problem in dynamic programming, which is aimed at determining the optimal cost of searching for an element in a Binary Search Tree (BST).
Before diving into the DP solution, letβs review some basic concepts related to BST:
- A BST is a binary tree where each node has a key greater than all the keys in the left sub-tree and less than all the keys in the right sub-tree.
- The keys of the BST are such that they will be accessed with some frequency. The more frequently accessed keys should be placed closer to the root of the tree to minimize the search cost.
- The search cost is the sum of the frequencies of all the keys that are accessed during a search.
So, given n keys and their frequencies, the task is to build a Binary Search Tree that has a minimal total search cost.
To solve this problem, we create a 2-D array βC[i][j]β where β0 <= i, j <= nβ representing the cost of the optimal BST containing keys βk[i+1]β through βk[j]β where βkβ is the array of keys sorted in increasing order. We also create a 2-D array βW[i][j]β for the sum of the frequencies from βk[i+1]β to βk[j]β.
The base case is βC[i][i] = W[i][i]β for all β0 i nβ.
For i > j, βC[i][j] = 0β as an empty tree has no cost.
For all i < j, the optimal cost is the minimum cost we can achieve by trying every possible root node in the BST of keys βk[i+1]β to βk[j]β. This optimal cost is given by the formula:
`C[i][j] = min(C[i][r-1] + C[r+1][j] + W[i][j])` where `i <= r <= j`
The above formula states that to compute the optimal cost for βk[i+1]β to βk[j]β, we have to consider every possible βrβ as root node and recursively compute the optimal cost for left and right subtrees of βrβ, then add the frequency of all nodes in βk[i+1]β to βk[j]β, which gives βW[i][j]β.
Once we have the above 2-D array βCβ, βC[0][n]β will give us the optimal cost of searching all elements in the BST.
Hereβs the Java code implementing the above algorithm:
public static int optimalSearchTree(int[] keys, int[] freq) {
int n = keys.length;
// create c[n+1][n+1] and w[n+1][n+1]
int[][] c = new int[n+1][n+1];
int[][] w = new int[n+1][n+1];
// initialize the base case when i=j
for (int i = 0; i < n; i++) {
c[i][i] = freq[i];
w[i][i] = freq[i];
}
// fill in the tables diagonally
for (int l = 2; l <= n+1; l++) {
for (int i = 0; i <= n-l+1; i++) {
int j = i + l - 1;
w[i][j] = w[i][j-1] + freq[j-1];
c[i][j] = Integer.MAX_VALUE;
// compute optimal cost for the subtree rooted at r
for (int r = i; r <= j; r++) {
int cost = ((r > i) ? c[i][r-1] : 0) +
((r < j) ? c[r+1][j] : 0) +
w[i][j];
if (cost < c[i][j])
c[i][j] = cost;
}
}
}
return c[0][n-1];
}
The time complexity of the above code is O(n3) which is due to the three nested for-loops. However, since we only have to evaluate βnβ *β (nβ +β 1)/2β subproblems, the space complexity is O(n2).