Given two integers n and k, find the lexicographically smallest string s of minimum possible length such that every possible string of length n formed using characters from 0 to k - 1 appears exactly once as a substring of s.
Examples:
Input: n = 2, k = 2
Output: "00110"
Explanation: The allowed characters are 0 and 1. All possible strings of length 2 are: "00", "01", "10", "11". The string "00110" contains all of them as substrings and has the minimum possible length.Input: n = 2, k = 3
Output: "0010211220"
Explanation: The allowed characters are 0, 1, and 2. All possible strings of length 2 are: "00", "01", "02", "10", "11", "12", "20", "21", "22". The string "0010211220" contains every possible length-2 string exactly once as a substring and has the minimum possible length.
Table of Content
[Expected Approach 1] Backtracking with DFS and Set - O(k^n * k * n) Time and O(k^n * n) Space
The idea is to start with n zeroes and keep adding one digit at a time. At every step, we check the last n - 1 characters and try to append a digit from 0 to k - 1. If the newly formed substring of length n has not been used before, we add it and continue DFS. Once all kn substrings are generated, the current string is a valid minimum length string.
#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
bool dfs(int n, int k, int total, string& ans, unordered_set<string>& visited) {
if ((int)visited.size() == total) return true;
string prefix = n > 1 ? ans.substr(ans.size() - n + 1) : "";
for (int digit = 0; digit < k; digit++) {
string curr = prefix + char(digit + '0');
if (!visited.count(curr)) {
visited.insert(curr);
ans.push_back(char(digit + '0'));
// Continue after adding a new substring.
if (dfs(n, k, total, ans, visited)) return true;
visited.erase(curr);
ans.pop_back();
}
}
return false;
}
string findString(int n, int k) {
int total = 1;
for (int i = 0; i < n; i++) total *= k;
string ans(n, '0');
unordered_set<string> visited;
visited.insert(ans);
dfs(n, k, total, ans, visited);
return ans;
}
int main() {
cout << findString(2, 2) << endl;
cout << findString(2, 3) << endl;
return 0;
}
import java.util.HashSet;
class GFG {
static boolean dfs(int n, int k, int total, StringBuilder ans, HashSet<String> visited) {
if (visited.size() == total) return true;
String prefix = n > 1 ? ans.substring(ans.length() - n + 1) : "";
for (int digit = 0; digit < k; digit++) {
String curr = prefix + (char)(digit + '0');
if (!visited.contains(curr)) {
visited.add(curr);
ans.append((char)(digit + '0'));
// Continue after adding a new substring.
if (dfs(n, k, total, ans, visited)) return true;
visited.remove(curr);
ans.deleteCharAt(ans.length() - 1);
}
}
return false;
}
static String findString(int n, int k) {
int total = 1;
for (int i = 0; i < n; i++) total *= k;
StringBuilder ans = new StringBuilder();
for (int i = 0; i < n; i++) ans.append('0');
HashSet<String> visited = new HashSet<>();
visited.add(ans.toString());
dfs(n, k, total, ans, visited);
return ans.toString();
}
public static void main(String[] args) {
System.out.println(findString(2, 2));
System.out.println(findString(2, 3));
}
}
def findString(n, k):
total = k ** n
ans = ["0"] * n
visited = {"".join(ans)}
def dfs():
if len(visited) == total:
return True
prefix = "".join(ans[-(n - 1):]) if n > 1 else ""
for digit in range(k):
curr = prefix + str(digit)
if curr not in visited:
visited.add(curr)
ans.append(str(digit))
# Continue after adding a new substring.
if dfs():
return True
visited.remove(curr)
ans.pop()
return False
dfs()
return "".join(ans)
if __name__ == "__main__":
print(findString(2, 2))
print(findString(2, 3))
using System;
using System.Text;
using System.Collections.Generic;
class GFG {
static bool dfs(int n, int k, int total, StringBuilder ans, HashSet<string> visited) {
if (visited.Count == total) return true;
string current = ans.ToString();
string prefix = n > 1 ? current.Substring(current.Length - n + 1) : "";
for (int digit = 0; digit < k; digit++) {
string curr = prefix + (char)(digit + '0');
if (!visited.Contains(curr)) {
visited.Add(curr);
ans.Append((char)(digit + '0'));
// Continue after adding a new substring.
if (dfs(n, k, total, ans, visited)) return true;
visited.Remove(curr);
ans.Length--;
}
}
return false;
}
static string findString(int n, int k) {
int total = 1;
for (int i = 0; i < n; i++) total *= k;
StringBuilder ans = new StringBuilder(new string('0', n));
HashSet<string> visited = new HashSet<string>();
visited.Add(ans.ToString());
dfs(n, k, total, ans, visited);
return ans.ToString();
}
static void Main() {
Console.WriteLine(findString(2, 2));
Console.WriteLine(findString(2, 3));
}
}
function dfs(n, k, total, ans, visited) {
if (visited.size === total) return true;
const prefix = n > 1 ? ans.slice(ans.length - n + 1).join("") : "";
for (let digit = 0; digit < k; digit++) {
const curr = prefix + digit;
if (!visited.has(curr)) {
visited.add(curr);
ans.push(String(digit));
// Continue after adding a new substring.
if (dfs(n, k, total, ans, visited)) return true;
visited.delete(curr);
ans.pop();
}
}
return false;
}
function findString(n, k) {
let total = 1;
for (let i = 0; i < n; i++) total *= k;
const ans = Array(n).fill("0");
const visited = new Set();
visited.add(ans.join(""));
dfs(n, k, total, ans, visited);
return ans.join("");
}
// Driver Code
console.log(findString(2, 2));
console.log(findString(2, 3));
Output
00110 0010211220
[Expected Approach 2] Greedy Construction using De Bruijn Sequence - O(k^n * k * n) Time and O(k^n * n) Space
We start with n zeroes and repeatedly try to append the largest possible digit that creates a new substring of length n. This ensures that we keep adding unused substrings until all k^n strings are covered.
Let us understand with an example:
For n = 2, k = 2
- Start with ans = "00"
- Visited substrings: {00}
- Append 1: 001, new substring 01
- Append 1: 0011, new substring 11
- Append 0: 00110, new substring 10
- All 4 substrings are covered: 00, 01, 11, 10.
- Final string: 00110
Why does this work?
- We start with a string of size
n, and every appended character creates a new substring. We use a hashset to ensure that we append only when a new substring is being generated. - By always choosing the extension that hasn’t been seen before, the algorithm guarantees that every n-digit string over {0,…,K−1} will appear exactly once in the shortest possible superstring.
- The total string length will be: k^n + (n-1) which is the minimum possible length.
#include <iostream>
#include <string>
#include <unordered_set>
#include <algorithm>
using namespace std;
void dfs(const string& node, int k, unordered_set<string>& visited, string& ans) {
for (int digit = 0; digit < k; digit++) {
string edge = node + char(digit + '0');
if (!visited.count(edge)) {
visited.insert(edge);
dfs(edge.substr(1), k, visited, ans);
// Add digit after completing this edge.
ans.push_back(char(digit + '0'));
}
}
}
string findString(int n, int k) {
string start(n - 1, '0');
unordered_set<string> visited;
string ans;
dfs(start, k, visited, ans);
ans += start;
reverse(ans.begin(), ans.end());
return ans;
}
int main() {
cout << findString(2, 2) << endl;
cout << findString(2, 3) << endl;
return 0;
}
import java.util.HashSet;
class GFG {
static void dfs(String node, int k, HashSet<String> visited, StringBuilder ans) {
for (int digit = 0; digit < k; digit++) {
String edge = node + (char)(digit + '0');
if (!visited.contains(edge)) {
visited.add(edge);
dfs(edge.substring(1), k, visited, ans);
// Add digit after completing this edge.
ans.append((char)(digit + '0'));
}
}
}
static String findString(int n, int k) {
String start = "0".repeat(n - 1);
HashSet<String> visited = new HashSet<>();
StringBuilder ans = new StringBuilder();
dfs(start, k, visited, ans);
ans.append(start);
return ans.reverse().toString();
}
public static void main(String[] args) {
System.out.println(findString(2, 2));
System.out.println(findString(2, 3));
}
}
def findString(n, k):
start = "0" * (n - 1)
visited = set()
ans = []
def dfs(node):
for digit in range(k):
edge = node + str(digit)
if edge not in visited:
visited.add(edge)
dfs(edge[1:])
# Add digit after completing this edge.
ans.append(str(digit))
dfs(start)
ans.append(start)
return "".join(ans)[::-1]
if __name__ == "__main__":
print(findString(2, 2))
print(findString(2, 3))
using System;
using System.Text;
using System.Collections.Generic;
class GFG {
static void dfs(string node, int k, HashSet<string> visited, StringBuilder ans) {
for (int digit = 0; digit < k; digit++) {
string edge = node + (char)(digit + '0');
if (!visited.Contains(edge)) {
visited.Add(edge);
dfs(edge.Substring(1), k, visited, ans);
// Add digit after completing this edge.
ans.Append((char)(digit + '0'));
}
}
}
static string findString(int n, int k) {
string start = new string('0', n - 1);
HashSet<string> visited = new HashSet<string>();
StringBuilder ans = new StringBuilder();
dfs(start, k, visited, ans);
ans.Append(start);
char[] chars = ans.ToString().ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
static void Main() {
Console.WriteLine(findString(2, 2));
Console.WriteLine(findString(2, 3));
}
}
function dfs(node, k, visited, ans) {
for (let digit = 0; digit < k; digit++) {
const edge = node + digit;
if (!visited.has(edge)) {
visited.add(edge);
dfs(edge.slice(1), k, visited, ans);
// Add digit after completing this edge.
ans.push(String(digit));
}
}
}
function findString(n, k) {
const start = "0".repeat(n - 1);
const visited = new Set();
const ans = [];
dfs(start, k, visited, ans);
ans.push(start);
return ans.join("").split("").reverse().join("");
}
// Driver Code
console.log(findString(2, 2));
console.log(findString(2, 3));
Output
00110 0010211220