Print kth Character

Last Updated : 2 Aug, 2026

Given a string s consisting of lowercase alphabetic characters, consider all unique substrings of s (i.e., no substring value is repeated, even if it occurs at multiple positions in s). Arrange these unique substrings in lexicographically sorted order, then concatenate them into a single string.

Find the k-th character (1-indexed) of this concatenated string.

Examples:  

Input: s = "banana", k = 10
Output: n
Explanation: The unique substrings of "banana", sorted lexicographically, are:
a, an, ana, anan, anana, b, ba, ban, bana, banan, banana, n, na, nan, nana
Concatenating them gives:
"a" + "an" + "ana" + "anan" + "anana" + "b" + "ba" + "ban" + "bana" + "banan" + "banana" + "n" + "na" + "nan" + "nana"
Tracking cumulative length as we concatenate:
"a" -> length 1 (cumulative: 1)
"an" -> length 2 (cumulative: 3)
"ana" -> length 3 (cumulative: 6)
"anan" -> length 4 (cumulative: 10)
The 10th character falls at the end of "anan", which is 'n'.
Hence, the answer is 'n'.

Input: s = "abcdefg", k = 10
Output: d
Explanation: Since all characters in "abcdefg" are distinct, every substring is already unique.
Sorted lexicographically, the substrings starting with 'a' come first:
a, ab, abc, abcd, abcde, abcdef, abcdefg, ...
Tracking cumulative length as we concatenate:
"a" -> length 1 (cumulative: 1)
"ab" -> length 2 (cumulative: 3)
"abc" -> length 3 (cumulative: 6)
"abcd" -> length 4 (cumulative: 10)
The 10th character falls at the end of "abcd", which is 'd'.
Hence, the answer is 'd'.

Try It Yourself
redirect icon

[Naive Approach] Generate All Unique Substrings and Sort - O(n ^ 3 log n) Time and O(n ^ 3) Space

The idea is to generate all substrings, store only unique ones in a set, sort them lexicographically, and traverse them while counting characters until the k-th character is reached.

Working of Approach:

  • Generate all possible substrings of the given string and insert them into a set to keep only unique substrings while maintaining lexicographical order.
  • Traverse the substrings stored in the set one by one.
  • Keep a running count of the total number of characters contributed by the processed substrings.
  • When the cumulative count reaches or exceeds k, the required character lies in the current substring.
  • Return the corresponding character using its position within the current substring.
C++
#include <bits/stdc++.h>
using namespace std;

char findKthChar(string &s, int k)
{
    set<string> st;
    int n = s.size();

    // Generate all substrings.
    for (int i = 0; i < n; i++)
    {
        string cur = "";
        for (int j = i; j < n; j++)
        {
            cur += s[j];
            st.insert(cur);
        }
    }

    // Traverse unique substrings in sorted order.
    int cnt = 0;
    for (auto &str : st)
    {
        if (cnt + str.size() >= k)
            return str[k - cnt - 1];
        cnt += str.size();
    }

    return '#';
}

int main()
{
    string s = "abcdefg";
    int k = 10;

    cout << findKthChar(s, k);

    return 0;
}
Java
import java.util.HashSet;
import java.util.Set;

public class GFG {

    public static char findKthChar(String s, int k)
    {
        Set<String> st = new HashSet<>();
        int n = s.length();

        // Generate all substrings.
        for (int i = 0; i < n; i++) {
            String cur = "";
            for (int j = i; j < n; j++) {
                cur += s.charAt(j);
                st.add(cur);
            }
        }

        // Traverse unique substrings in sorted order.
        int cnt = 0;
        String[] sortedStrings = st.toArray(new String[0]);
        java.util.Arrays.sort(sortedStrings);
        for (String str : sortedStrings) {
            if (cnt + str.length() >= k)
                return str.charAt(k - cnt - 1);
            cnt += str.length();
        }

        return '#';
    }

    public static void main(String[] args)
    {
        String s = "abcdefg";
        int k = 10;

        System.out.println(findKthChar(s, k));
    }
}
Python
def findKthChar(s, k):
    st = set()
    n = len(s)

    # Generate all substrings.
    for i in range(n):
        cur = ""
        for j in range(i, n):
            cur += s[j]
            st.add(cur)

    # Traverse unique substrings in sorted order.
    cnt = 0
    sorted_strings = sorted(st)
    for str_ in sorted_strings:
        if cnt + len(str_) >= k:
            return str_[k - cnt - 1]
        cnt += len(str_)

    return '#'


if __name__ == "__main__":
    s = "abcdefg"
    k = 10

    print(findKthChar(s, k))
C#
using System;
using System.Collections.Generic;

class Program {
    static char findKthChar(string s, int k)
    {
        HashSet<string> st = new HashSet<string>();
        int n = s.Length;

        // Generate all substrings.
        for (int i = 0; i < n; i++) {
            string cur = "";
            for (int j = i; j < n; j++) {
                cur += s[j];
                st.Add(cur);
            }
        }

        // Traverse unique substrings in sorted order.
        List<string> sortedStrings = new List<string>(st);
        sortedStrings.Sort();

        int cnt = 0;
        foreach(string str in sortedStrings)
        {
            if (cnt + str.Length >= k)
                return str[k - cnt - 1];

            cnt += str.Length;
        }

        return '#';
    }

    static void Main()
    {
        string s = "abcdefg";
        int k = 10;

        Console.WriteLine(findKthChar(s, k));
    }
}
JavaScript
function findKthChar(s, k)
{
    const st = new Set();
    const n = s.length;

    // Generate all substrings.
    for (let i = 0; i < n; i++) {
        let cur = "";
        for (let j = i; j < n; j++) {
            cur += s[j];
            st.add(cur);
        }
    }

    // Traverse unique substrings in sorted order.
    let cnt = 0;
    const sortedStrings = Array.from(st).sort();
    for (const str of sortedStrings) {
        if (cnt + str.length >= k)
            return str[k - cnt - 1];
        cnt += str.length;
    }

    return "#";
}

// Driver Code
const s = "abcdefg";
const k = 10;
console.log(findKthChar(s, k));

Output
d

[Expected Approach] Using Suffix Array and LCP Array - O(n log ^ 2 n) Time and O(n) Space

The idea is to process suffixes in lexicographical order using a suffix array. The LCP array tells how many prefixes are already counted, so only new prefixes contribute unique substrings. Count their contribution without generating substrings and locate the required character using binary search.

Working of Approach:

  • Build the suffix array to arrange all suffixes of the string in lexicographical order and compute the LCP array using Kasai's algorithm.
  • Every unique substring is a prefix of some suffix. For each suffix, only prefixes longer than its LCP with the previous suffix are new unique substrings.
  • Compute the total number of characters contributed by these new prefixes using the sum of prefix lengths, without generating the substrings explicitly.
  • If the required k lies within the current suffix's contribution, use binary search to find the exact prefix containing the k-th character.
  • Return the corresponding character from the original string; otherwise, skip the current suffix's contribution and continue.

Let us understand with an example:
Input: s = "abcdefg", k = 10

  • For s = "abcdefg", the suffix array stores all suffixes in lexicographical order as: abcdefg, bcdefg, cdefg, defg, efg, fg, g. The corresponding LCP values are all 0.
  • The first suffix "abcdefg" contributes the unique prefixes: a, ab, abc, abcd, abcde, abcdef, abcdefg, whose total contribution is 1 + 2 + ... + 7 = 28 characters.
  • Since k = 10 ≤ 28, the required character lies within the contribution of the first suffix itself.
  • Binary search finds that the cumulative length first reaches 10 at the prefix "abcd" (length 4), since 1 + 2 + 3 + 4 = 10.
  • The 10th character is the last character of "abcd", which is 'd'.
C++
#include <bits/stdc++.h>
using namespace std;

// Builds the suffix array using the prefix doubling algorithm.
// Sentinel '$' appended so no suffix is a prefix of another.
vector<int> buildSuffixArray(string &s)
{
    string t = s + "$";
    int n = t.length();
    vector<int> p(n), c(n), cnt(max(256, n), 0);

    // Initial sort by single character
    for (int i = 0; i < n; i++)
        cnt[t[i]]++;
    for (int i = 1; i < 256; i++)
        cnt[i] += cnt[i - 1];
    for (int i = 0; i < n; i++)
        p[--cnt[t[i]]] = i;

    c[p[0]] = 0;
    int cls = 1;
    for (int i = 1; i < n; i++)
    {
        if (t[p[i]] != t[p[i - 1]])
            cls++;
        c[p[i]] = cls - 1;
    }

    vector<int> pn(n), cn(n);
    for (int h = 0; (1 << h) < n; ++h)
    {
        // pn[i]: suffix that pairs with p[i] to compare 2^(h+1)-length prefixes
        for (int i = 0; i < n; i++)
        {
            pn[i] = p[i] - (1 << h);
            if (pn[i] < 0)
                pn[i] += n;
        }

        fill(cnt.begin(), cnt.begin() + cls, 0);
        for (int i = 0; i < n; i++)
            cnt[c[pn[i]]]++;
        for (int i = 1; i < cls; i++)
            cnt[i] += cnt[i - 1];
        for (int i = n - 1; i >= 0; i--)
            p[--cnt[c[pn[i]]]] = pn[i];

        cn[p[0]] = 0;
        cls = 1;
        for (int i = 1; i < n; i++)
        {
            int a1 = c[p[i]], a2 = c[(p[i] + (1 << h)) % n];
            int b1 = c[p[i - 1]], b2 = c[(p[i - 1] + (1 << h)) % n];
            if (a1 != b1 || a2 != b2)
                cls++;
            cn[p[i]] = cls - 1;
        }
        c = cn;
    }

    // Sentinel is always lexicographically smallest, so it's at p[0] — remove it.
    p.erase(p.begin());
    return p;
}

// Kasai's Algorithm: builds LCP array in O(n).
// lcp[i] = longest common prefix between sorted suffixes at position i and i+1.
vector<int> kasai(const string &s, const vector<int> &sa)
{
    int n = sa.size();
    vector<int> lcp(n, 0), rank(n, 0);
    for (int i = 0; i < n; i++)
        rank[sa[i]] = i;

    int h = 0;
    for (int i = 0; i < n; i++)
    {
        if (rank[i] == n - 1)
        {
            h = 0;
            continue;
        }
        int j = sa[rank[i] + 1];
        while (i + h < n && j + h < n && s[i + h] == s[j + h])
            h++;
        lcp[rank[i]] = h;
        if (h > 0)
            h--; // carries forward, bounding total work to O(n)
    }
    return lcp;
}

// Finds the k-th character (1-indexed) of the concatenation of all
// unique substrings of s in sorted order, without building it explicitly.
// Idea: every unique substring is a prefix of some suffix. Each sorted
// suffix only contributes NEW substrings beyond its LCP with the previous
// suffix, so we can skip whole suffix "blocks" using triangular-sum counts.
char findKthChar(string &s, int k)
{
    int n = s.length();
    vector<int> sa = buildSuffixArray(s);
    vector<int> lcp = kasai(s, sa);

    for (int i = 0; i < n; i++)
    {
        int len = n - sa[i];
        int l = (i == 0) ? 0 : lcp[i - 1];

        if (len <= l)
            continue;

        // Total chars contributed by this suffix's new unique prefixes: sum(l+1 ..
        // len)
        int skip = (len * (len + 1)) / 2 - (l * (l + 1)) / 2;

        if (k <= skip)
        {
            // Binary search the shortest prefix length whose cumulative count
            // reaches k
            int lo = l + 1, hi = len, bestLen = len;
            while (lo <= hi)
            {
                int mid = lo + (hi - lo) / 2;
                int cnt = (mid * (mid + 1)) / 2 - (l * (l + 1)) / 2;
                if (cnt >= k)
                {
                    bestLen = mid;
                    hi = mid - 1;
                }
                else
                {
                    lo = mid + 1;
                }
            }

            int prev = ((bestLen - 1) * bestLen) / 2 - (l * (l + 1)) / 2;
            int rem = k - prev;
            return s[sa[i] + rem - 1];
        }
        else
        {
            k -= skip;
        }
    }

    // unreachable given valid k per constraints
    return ' ';
}

int main()
{
    string s = "abcdefg";
    int k = 10;

    cout << findKthChar(s, k);

    return 0;
}
Java
import java.util.*;

class GFG {

    // Builds the suffix array using the prefix doubling
    // algorithm. Sentinel '$' appended so no suffix is a
    // prefix of another.
    static ArrayList<Integer> buildSuffixArray(String s)
    {
        String t = s + "$";
        int n = t.length();

        ArrayList<Integer> p
            = new ArrayList<>(Collections.nCopies(n, 0));
        ArrayList<Integer> c
            = new ArrayList<>(Collections.nCopies(n, 0));
        int[] cnt = new int[Math.max(256, n)];

        // Initial sort by single character
        for (int i = 0; i < n; i++)
            cnt[t.charAt(i)]++;

        for (int i = 1; i < 256; i++)
            cnt[i] += cnt[i - 1];

        for (int i = 0; i < n; i++)
            p.set(--cnt[t.charAt(i)], i);

        c.set(p.get(0), 0);
        int cls = 1;

        for (int i = 1; i < n; i++) {
            if (t.charAt(p.get(i))
                != t.charAt(p.get(i - 1)))
                cls++;
            c.set(p.get(i), cls - 1);
        }

        ArrayList<Integer> pn
            = new ArrayList<>(Collections.nCopies(n, 0));
        ArrayList<Integer> cn
            = new ArrayList<>(Collections.nCopies(n, 0));

        for (int h = 0; (1 << h) < n; h++) {

            // pn[i]: suffix that pairs with p[i] to compare
            // 2^(h+1)-length prefixes
            for (int i = 0; i < n; i++) {
                int val = p.get(i) - (1 << h);
                if (val < 0)
                    val += n;
                pn.set(i, val);
            }

            Arrays.fill(cnt, 0, cls, 0);

            for (int i = 0; i < n; i++)
                cnt[c.get(pn.get(i))]++;

            for (int i = 1; i < cls; i++)
                cnt[i] += cnt[i - 1];

            for (int i = n - 1; i >= 0; i--)
                p.set(--cnt[c.get(pn.get(i))], pn.get(i));

            cn.set(p.get(0), 0);
            cls = 1;

            for (int i = 1; i < n; i++) {
                int a1 = c.get(p.get(i));
                int a2 = c.get((p.get(i) + (1 << h)) % n);
                int b1 = c.get(p.get(i - 1));
                int b2
                    = c.get((p.get(i - 1) + (1 << h)) % n);

                if (a1 != b1 || a2 != b2)
                    cls++;

                cn.set(p.get(i), cls - 1);
            }

            c = new ArrayList<>(cn);
        }

        // Sentinel is always lexicographically smallest, so
        // it's at p[0] — remove it.
        p.remove(0);
        return p;
    }

    // Kasai's Algorithm: builds LCP array in O(n).
    // lcp[i] = longest common prefix between sorted
    // suffixes at position i and i+1.
    static ArrayList<Integer> kasai(String s,
                                    ArrayList<Integer> sa)
    {
        int n = sa.size();

        ArrayList<Integer> lcp
            = new ArrayList<>(Collections.nCopies(n, 0));
        ArrayList<Integer> rank
            = new ArrayList<>(Collections.nCopies(n, 0));

        for (int i = 0; i < n; i++)
            rank.set(sa.get(i), i);

        int h = 0;

        for (int i = 0; i < n; i++) {

            if (rank.get(i) == n - 1) {
                h = 0;
                continue;
            }

            int j = sa.get(rank.get(i) + 1);

            while (i + h < n && j + h < n
                   && s.charAt(i + h) == s.charAt(j + h))
                h++;

            lcp.set(rank.get(i), h);

            if (h > 0)
                h--; // carries forward, bounding total work
                     // to O(n)
        }

        return lcp;
    }

    // Finds the k-th character (1-indexed) of the
    // concatenation of all unique substrings of s in sorted
    // order, without building it explicitly. Idea: every
    // unique substring is a prefix of some suffix. Each
    // sorted suffix only contributes NEW substrings beyond
    // its LCP with the previous suffix, so we can skip
    // whole suffix "blocks" using triangular-sum counts.
    static char findKthChar(String s, int k)
    {

        int n = s.length();

        ArrayList<Integer> sa = buildSuffixArray(s);
        ArrayList<Integer> lcp = kasai(s, sa);

        for (int i = 0; i < n; i++) {

            int len = n - sa.get(i);
            int l = (i == 0) ? 0 : lcp.get(i - 1);

            if (len <= l)
                continue;

            // Total chars contributed by this suffix's new
            // unique prefixes: sum(l+1 .. len)
            int skip
                = (len * (len + 1)) / 2 - (l * (l + 1)) / 2;

            if (k <= skip) {

                // Binary search the shortest prefix length
                // whose cumulative count reaches k
                int lo = l + 1, hi = len, bestLen = len;

                while (lo <= hi) {
                    int mid = lo + (hi - lo) / 2;
                    int cnt = (mid * (mid + 1)) / 2
                              - (l * (l + 1)) / 2;

                    if (cnt >= k) {
                        bestLen = mid;
                        hi = mid - 1;
                    }
                    else {
                        lo = mid + 1;
                    }
                }

                int prev = ((bestLen - 1) * bestLen) / 2
                           - (l * (l + 1)) / 2;

                int rem = k - prev;

                return s.charAt(sa.get(i) + rem - 1);
            }
            else {
                k -= skip;
            }
        }

        // unreachable given valid k per constraints
        return ' ';
    }

    public static void main(String[] args)
    {

        String s = "abcdefg";
        int k = 10;

        System.out.println(findKthChar(s, k));
    }
}
Python
# Builds the suffix array using the prefix doubling algorithm.
# Sentinel '$' appended so no suffix is a prefix of another.
def buildSuffixArray(s):
    t = s + "$"
    n = len(t)

    p = [0] * n
    c = [0] * n
    cnt = [0] * max(256, n)

    # Initial sort by single character
    for ch in t:
        cnt[ord(ch)] += 1

    for i in range(1, 256):
        cnt[i] += cnt[i - 1]

    for i in range(n):
        cnt[ord(t[i])] -= 1
        p[cnt[ord(t[i])]] = i

    c[p[0]] = 0
    cls = 1

    for i in range(1, n):
        if t[p[i]] != t[p[i - 1]]:
            cls += 1
        c[p[i]] = cls - 1

    pn = [0] * n
    cn = [0] * n

    h = 0
    while (1 << h) < n:

        # pn[i]: suffix that pairs with p[i] to compare 2^(h+1)-length prefixes
        for i in range(n):
            pn[i] = p[i] - (1 << h)
            if pn[i] < 0:
                pn[i] += n

        cnt = [0] * max(256, n)

        for i in range(n):
            cnt[c[pn[i]]] += 1

        for i in range(1, cls):
            cnt[i] += cnt[i - 1]

        for i in range(n - 1, -1, -1):
            cnt[c[pn[i]]] -= 1
            p[cnt[c[pn[i]]]] = pn[i]

        cn[p[0]] = 0
        cls = 1

        for i in range(1, n):
            a1 = c[p[i]]
            a2 = c[(p[i] + (1 << h)) % n]
            b1 = c[p[i - 1]]
            b2 = c[(p[i - 1] + (1 << h)) % n]

            if a1 != b1 or a2 != b2:
                cls += 1

            cn[p[i]] = cls - 1

        c = cn[:]
        h += 1

    # Sentinel is always lexicographically smallest, so it's at p[0] — remove it.
    return p[1:]


# Kasai's Algorithm: builds LCP array in O(n).
# lcp[i] = longest common prefix between sorted suffixes at position i and i+1.
def kasai(s, sa):
    n = len(sa)

    lcp = [0] * n
    rank = [0] * n

    for i in range(n):
        rank[sa[i]] = i

    h = 0

    for i in range(n):

        if rank[i] == n - 1:
            h = 0
            continue

        j = sa[rank[i] + 1]

        while i + h < n and j + h < n and s[i + h] == s[j + h]:
            h += 1

        lcp[rank[i]] = h

        if h > 0:
            h -= 1  # carries forward, bounding total work to O(n)

    return lcp


# Finds the k-th character (1-indexed) of the concatenation of all
# unique substrings of s in sorted order, without building it explicitly.
# Idea: every unique substring is a prefix of some suffix. Each sorted
# suffix only contributes NEW substrings beyond its LCP with the previous
# suffix, so we can skip whole suffix "blocks" using triangular-sum counts.
def findKthChar(s, k):
    n = len(s)

    sa = buildSuffixArray(s)
    lcp = kasai(s, sa)

    for i in range(n):

        length = n - sa[i]
        l = 0 if i == 0 else lcp[i - 1]

        if length <= l:
            continue

        # Total chars contributed by this suffix's new unique prefixes:
        # sum(l+1 .. length)
        skip = (length * (length + 1)) // 2 - (l * (l + 1)) // 2

        if k <= skip:

            # Binary search the shortest prefix length whose cumulative count
            # reaches k
            lo = l + 1
            hi = length
            bestLen = length

            while lo <= hi:
                mid = lo + (hi - lo) // 2
                cnt = (mid * (mid + 1)) // 2 - (l * (l + 1)) // 2

                if cnt >= k:
                    bestLen = mid
                    hi = mid - 1
                else:
                    lo = mid + 1

            prev = ((bestLen - 1) * bestLen) // 2 - (l * (l + 1)) // 2
            rem = k - prev

            return s[sa[i] + rem - 1]

        else:
            k -= skip

    # unreachable given valid k per constraints
    return ' '


if __name__ == "__main__":
    s = "abcdefg"
    k = 10

    print(findKthChar(s, k))
C#
using System;
using System.Collections.Generic;

class GFG {
    // Builds the suffix array using the prefix doubling
    // algorithm. Sentinel '$' appended so no suffix is a
    // prefix of another.
    static List<int> BuildSuffixArray(string s)
    {
        string t = s + "$";
        int n = t.Length;

        List<int> p = new List<int>(new int[n]);
        List<int> c = new List<int>(new int[n]);
        int[] cnt = new int[Math.Max(256, n)];

        // Initial sort by single character
        for (int i = 0; i < n; i++)
            cnt[t[i]]++;

        for (int i = 1; i < 256; i++)
            cnt[i] += cnt[i - 1];

        for (int i = 0; i < n; i++)
            p[--cnt[t[i]]] = i;

        c[p[0]] = 0;
        int cls = 1;

        for (int i = 1; i < n; i++) {
            if (t[p[i]] != t[p[i - 1]])
                cls++;

            c[p[i]] = cls - 1;
        }

        List<int> pn = new List<int>(new int[n]);
        List<int> cn = new List<int>(new int[n]);

        for (int h = 0; (1 << h) < n; h++) {
            // pn[i]: suffix that pairs with p[i] to compare
            // 2^(h+1)-length prefixes
            for (int i = 0; i < n; i++) {
                pn[i] = p[i] - (1 << h);
                if (pn[i] < 0)
                    pn[i] += n;
            }

            Array.Clear(cnt, 0, cls);

            for (int i = 0; i < n; i++)
                cnt[c[pn[i]]]++;

            for (int i = 1; i < cls; i++)
                cnt[i] += cnt[i - 1];

            for (int i = n - 1; i >= 0; i--)
                p[--cnt[c[pn[i]]]] = pn[i];

            cn[p[0]] = 0;
            cls = 1;

            for (int i = 1; i < n; i++) {
                int a1 = c[p[i]];
                int a2 = c[(p[i] + (1 << h)) % n];
                int b1 = c[p[i - 1]];
                int b2 = c[(p[i - 1] + (1 << h)) % n];

                if (a1 != b1 || a2 != b2)
                    cls++;

                cn[p[i]] = cls - 1;
            }

            c = new List<int>(cn);
        }

        // Sentinel is always lexicographically smallest, so
        // it's at p[0] — remove it.
        p.RemoveAt(0);

        return p;
    }

    // Kasai's Algorithm: builds LCP array in O(n).
    // lcp[i] = longest common prefix between sorted
    // suffixes at position i and i+1.
    static List<int> Kasai(string s, List<int> sa)
    {
        int n = sa.Count;

        List<int> lcp = new List<int>(new int[n]);
        List<int> rank = new List<int>(new int[n]);

        for (int i = 0; i < n; i++)
            rank[sa[i]] = i;

        int h = 0;

        for (int i = 0; i < n; i++) {
            if (rank[i] == n - 1) {
                h = 0;
                continue;
            }

            int j = sa[rank[i] + 1];

            while (i + h < n && j + h < n
                   && s[i + h] == s[j + h])
                h++;

            lcp[rank[i]] = h;

            if (h > 0)
                h--; // carries forward, bounding total work
                     // to O(n)
        }

        return lcp;
    }

    // Finds the k-th character (1-indexed) of the
    // concatenation of all unique substrings of s in sorted
    // order, without building it explicitly. Idea: every
    // unique substring is a prefix of some suffix. Each
    // sorted suffix only contributes NEW substrings beyond
    // its LCP with the previous suffix, so we can skip
    // whole suffix "blocks" using triangular-sum counts.
    static char findKthChar(string s, int k)
    {
        int n = s.Length;

        List<int> sa = BuildSuffixArray(s);
        List<int> lcp = Kasai(s, sa);

        for (int i = 0; i < n; i++) {
            int len = n - sa[i];
            int l = (i == 0) ? 0 : lcp[i - 1];

            if (len <= l)
                continue;

            // Total chars contributed by this suffix's new
            // unique prefixes: sum(l+1 .. len)
            int skip
                = (len * (len + 1)) / 2 - (l * (l + 1)) / 2;

            if (k <= skip) {
                // Binary search the shortest prefix length
                // whose cumulative count reaches k
                int lo = l + 1, hi = len, bestLen = len;

                while (lo <= hi) {
                    int mid = lo + (hi - lo) / 2;
                    int cnt = (mid * (mid + 1)) / 2
                              - (l * (l + 1)) / 2;

                    if (cnt >= k) {
                        bestLen = mid;
                        hi = mid - 1;
                    }
                    else {
                        lo = mid + 1;
                    }
                }

                int prev = ((bestLen - 1) * bestLen) / 2
                           - (l * (l + 1)) / 2;

                int rem = k - prev;

                return s[sa[i] + rem - 1];
            }
            else {
                k -= skip;
            }
        }

        // unreachable given valid k per constraints
        return ' ';
    }

    static void Main()
    {
        string s = "abcdefg";
        int k = 10;

        Console.WriteLine(findKthChar(s, k));
    }
}
JavaScript
// Builds the suffix array using the prefix doubling
// algorithm. Sentinel '$' appended so no suffix is a prefix
// of another.
function buildSuffixArray(s)
{
    let t = s + "$";
    let n = t.length;

    let p = new Array(n).fill(0);
    let c = new Array(n).fill(0);
    let cnt = new Array(Math.max(256, n)).fill(0);

    // Initial sort by single character
    for (let i = 0; i < n; i++)
        cnt[t.charCodeAt(i)]++;

    for (let i = 1; i < 256; i++)
        cnt[i] += cnt[i - 1];

    for (let i = 0; i < n; i++)
        p[--cnt[t.charCodeAt(i)]] = i;

    c[p[0]] = 0;
    let cls = 1;

    for (let i = 1; i < n; i++) {
        if (t[p[i]] !== t[p[i - 1]])
            cls++;
        c[p[i]] = cls - 1;
    }

    let pn = new Array(n).fill(0);
    let cn = new Array(n).fill(0);

    for (let h = 0; (1 << h) < n; h++) {

        // pn[i]: suffix that pairs with p[i] to compare
        // 2^(h+1)-length prefixes
        for (let i = 0; i < n; i++) {
            pn[i] = p[i] - (1 << h);
            if (pn[i] < 0)
                pn[i] += n;
        }

        cnt.fill(0, 0, cls);

        for (let i = 0; i < n; i++)
            cnt[c[pn[i]]]++;

        for (let i = 1; i < cls; i++)
            cnt[i] += cnt[i - 1];

        for (let i = n - 1; i >= 0; i--)
            p[--cnt[c[pn[i]]]] = pn[i];

        cn[p[0]] = 0;
        cls = 1;

        for (let i = 1; i < n; i++) {
            let a1 = c[p[i]];
            let a2 = c[(p[i] + (1 << h)) % n];
            let b1 = c[p[i - 1]];
            let b2 = c[(p[i - 1] + (1 << h)) % n];

            if (a1 !== b1 || a2 !== b2)
                cls++;

            cn[p[i]] = cls - 1;
        }

        c = [...cn ];
    }

    // Sentinel is always lexicographically smallest, so
    // it's at p[0] — remove it.
    p.shift();

    return p;
}

// Kasai's Algorithm: builds LCP array in O(n).
// lcp[i] = longest common prefix between sorted suffixes at
// position i and i+1.
function kasai(s, sa)
{
    let n = sa.length;

    let lcp = new Array(n).fill(0);
    let rank = new Array(n).fill(0);

    for (let i = 0; i < n; i++)
        rank[sa[i]] = i;

    let h = 0;

    for (let i = 0; i < n; i++) {

        if (rank[i] === n - 1) {
            h = 0;
            continue;
        }

        let j = sa[rank[i] + 1];

        while (i + h < n && j + h < n
               && s[i + h] === s[j + h])
            h++;

        lcp[rank[i]] = h;

        if (h > 0)
            h--; // carries forward, bounding total work to
                 // O(n)
    }

    return lcp;
}

// Finds the k-th character (1-indexed) of the concatenation
// of all unique substrings of s in sorted order, without
// building it explicitly. Idea: every unique substring is a
// prefix of some suffix. Each sorted suffix only
// contributes NEW substrings beyond its LCP with the
// previous suffix, so we can skip whole suffix "blocks"
// using triangular-sum counts.
function findKthChar(s, k)
{

    let n = s.length;

    let sa = buildSuffixArray(s);
    let lcp = kasai(s, sa);

    for (let i = 0; i < n; i++) {

        let len = n - sa[i];
        let l = (i === 0) ? 0 : lcp[i - 1];

        if (len <= l)
            continue;

        // Total chars contributed by this suffix's new
        // unique prefixes: sum(l+1 .. len)
        let skip
            = (len * (len + 1)) / 2 - (l * (l + 1)) / 2;

        if (k <= skip) {

            // Binary search the shortest prefix length
            // whose cumulative count reaches k
            let lo = l + 1;
            let hi = len;
            let bestLen = len;

            while (lo <= hi) {
                let mid = lo + Math.floor((hi - lo) / 2);
                let cnt = (mid * (mid + 1)) / 2
                          - (l * (l + 1)) / 2;

                if (cnt >= k) {
                    bestLen = mid;
                    hi = mid - 1;
                }
                else {
                    lo = mid + 1;
                }
            }

            let prev = ((bestLen - 1) * bestLen) / 2
                       - (l * (l + 1)) / 2;
            let rem = k - prev;

            return s[sa[i] + rem - 1];
        }
        else {
            k -= skip;
        }
    }

    // unreachable given valid k per constraints
    return " ";
}

// Driver code
let s = "abcdefg";
let k = 10;

console.log(findKthChar(s, k));

Output
d
Comment