LeetCode 388 - Longest Absolute File Path
The problem gives us a string representation of a file system hierarchy. Every line represents either a directory or a file. The hierarchy is encoded using newline characters (n) to separate entries and tab characters (t) to indicate nesting depth.
Difficulty: 🟡 Medium
Topics: String, Stack, Depth-First Search
Solution
Problem Understanding
The problem gives us a string representation of a file system hierarchy. Every line represents either a directory or a file. The hierarchy is encoded using newline characters (\n) to separate entries and tab characters (\t) to indicate nesting depth.
A line with no leading tabs exists at depth 0, meaning it is directly under the root. A line with one leading tab exists inside the previous depth 0 directory, a line with two tabs exists one level deeper, and so on.
A file is identified by the presence of a dot (.) in its name. Directories do not contain dots. The task is to compute the length of the longest absolute path that leads to a file.
For example, consider this structure:
dir
subdir2
file.ext
The corresponding absolute path is:
dir/subdir2/file.ext
The answer is the number of characters in that path, including the / separators.
The constraints are important because the input length can be up to 10^4, which means we should avoid repeatedly rebuilding long strings or performing expensive recursive traversals. A linear-time solution is expected.
There are several edge cases that can easily cause bugs if handled incorrectly.
A filesystem may contain no files at all. In that case, the correct answer is 0.
A file may exist directly under the root directory without nested folders.
Multiple directories may exist at the same depth, so we need to correctly discard previous branches when moving back up the hierarchy.
Directory names and file names may contain spaces and digits, which means we cannot rely on simplistic parsing assumptions other than tabs and newlines.
The input is guaranteed to represent a valid filesystem, and every file or directory name has positive length.
Approaches
Brute Force Approach
A straightforward approach is to explicitly reconstruct the full path for every file.
We can process each line one by one, determine its depth from the number of leading tabs, and maintain the current directory hierarchy in a stack. Whenever we encounter a file, we join all directory names and the file name together using / separators and compute the resulting string length.
This approach is correct because the stack always represents the current path from the root to the current entry. Every time we see a file, constructing the path from the stack gives the exact absolute path.
However, this method is inefficient because joining strings repeatedly can become expensive. If there are many deeply nested files, we may repeatedly rebuild nearly identical paths. In the worst case, each file path construction costs O(n), leading to quadratic complexity overall.
Optimal Approach
The key insight is that we do not actually need to construct the full path strings. We only need their lengths.
Instead of storing full path components, we can store the cumulative path length at each depth level.
For every line:
- Determine its depth
- Remove the leading tabs
- Compute the current path length based on the parent directory length
- If the entry is a file, update the maximum answer
- If the entry is a directory, store its cumulative length for future children
This works because every absolute path is simply:
parent_path + "/" + current_name
So if we already know the total length up to the parent directory, we can compute the new length in constant time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeatedly rebuilds full path strings |
| Optimal | O(n) | O(n) | Stores cumulative path lengths by depth |
Algorithm Walkthrough
- Split the input string by newline characters.
Each resulting line represents either a file or a directory. 2. Maintain a mapping from depth to cumulative path length.
For example:
depth_lengths[2] = 15
means that the total length of the absolute path up to depth 2 is 15.
3. Initialize the root depth length.
We set:
depth_lengths[0] = 0
This represents an empty base path before processing any entries. 4. Process each line individually.
For every line:
- Count the number of leading
\tcharacters - This count represents the depth
- Remove the tabs to obtain the actual name
- Compute the current path length.
Suppose the current entry is at depth d.
Its parent directory is at depth d - 1.
The total path length becomes:
parent_length + len(name)
If the depth is greater than 0, we also add 1 for the / separator.
6. Check whether the entry is a file.
If the name contains a dot (.), it is a file.
Update the maximum path length if this path is longer than the previous best. 7. Otherwise, store the directory length.
If the entry is a directory, future children will use this path length as their parent length. 8. Continue until all lines are processed. 9. Return the maximum file path length found.
Why it works
The algorithm maintains the invariant that depth_lengths[d] always stores the total absolute path length up to depth d.
When processing a child node, its full path length depends only on its parent path length and its own name length. Since the parent length is already known, each new path length can be computed in constant time.
Because every line is processed exactly once, and because the cumulative lengths always reflect the correct filesystem hierarchy, the algorithm correctly computes the longest absolute file path.
Python Solution
class Solution:
def lengthLongestPath(self, input: str) -> int:
depth_lengths = {0: 0}
max_length = 0
for line in input.split("\n"):
depth = line.count("\t")
name = line.lstrip("\t")
current_length = depth_lengths[depth] + len(name)
if "." in name:
max_length = max(max_length, current_length)
else:
depth_lengths[depth + 1] = current_length + 1
return max_length
The implementation begins by creating a dictionary called depth_lengths. This dictionary maps each depth level to the cumulative path length up to that depth.
The value for depth 0 is initialized to 0, which represents the empty starting point before any directories are processed.
The input string is split using newline characters so that each filesystem entry can be processed independently.
For every line, the number of leading tabs determines the depth. The actual file or directory name is obtained by removing those tabs.
The variable current_length represents the total path length for the current entry. It is computed using the stored parent length plus the length of the current name.
If the current entry contains a dot, it is treated as a file. The algorithm updates max_length if this file path is the longest encountered so far.
Otherwise, the entry is a directory. In that case, the algorithm stores the cumulative length for the next depth level. The extra 1 accounts for the / separator that future children will require.
Finally, the algorithm returns the maximum file path length found during traversal.
Go Solution
package main
import (
"strings"
)
func lengthLongestPath(input string) int {
depthLengths := map[int]int{
0: 0,
}
maxLength := 0
lines := strings.Split(input, "\n")
for _, line := range lines {
depth := strings.Count(line, "\t")
name := strings.TrimLeft(line, "\t")
currentLength := depthLengths[depth] + len(name)
if strings.Contains(name, ".") {
if currentLength > maxLength {
maxLength = currentLength
}
} else {
depthLengths[depth+1] = currentLength + 1
}
}
return maxLength
}
The Go implementation follows the same logic as the Python version. A map stores cumulative path lengths indexed by depth.
The primary language-specific differences are related to string handling. Go uses functions from the strings package to count tabs, remove leading tabs, and detect file names.
Go strings are immutable, just like Python strings, so operations such as trimming and splitting produce new values.
No special integer overflow handling is needed because the maximum input size is small relative to Go's integer capacity.
Worked Examples
Example 1
Input:
dir
subdir1
subdir2
file.ext
Actual encoded input:
"dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext"
Step-by-step Trace
| Line | Depth | Name | Current Length | File? | depth_lengths | max_length |
|---|---|---|---|---|---|---|
| dir | 0 | dir | 3 | No | {0:0, 1:4} | 0 |
| \tsubdir1 | 1 | subdir1 | 11 | No | {0:0, 1:4, 2:12} | 0 |
| \tsubdir2 | 1 | subdir2 | 11 | No | {0:0, 1:4, 2:12} | 0 |
| \t\tfile.ext | 2 | file.ext | 20 | Yes | unchanged | 20 |
Final answer:
20
The longest path is:
dir/subdir2/file.ext
Example 2
Input:
dir
subdir1
file1.ext
subsubdir1
subdir2
subsubdir2
file2.ext
Step-by-step Trace
| Line | Depth | Name | Current Length | File? | max_length |
|---|---|---|---|---|---|
| dir | 0 | dir | 3 | No | 0 |
| \tsubdir1 | 1 | subdir1 | 11 | No | 0 |
| \t\tfile1.ext | 2 | file1.ext | 21 | Yes | 21 |
| \t\tsubsubdir1 | 2 | subsubdir1 | 22 | No | 21 |
| \tsubdir2 | 1 | subdir2 | 11 | No | 21 |
| \t\tsubsubdir2 | 2 | subsubdir2 | 22 | No | 21 |
| \t\t\tfile2.ext | 3 | file2.ext | 32 | Yes | 32 |
Final answer:
32
The longest path is:
dir/subdir2/subsubdir2/file2.ext
Example 3
Input:
"a"
Step-by-step Trace
| Line | Depth | Name | Current Length | File? | max_length |
|---|---|---|---|---|---|
| a | 0 | a | 1 | No | 0 |
No files are present, so the answer remains:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each character is processed a constant number of times |
| Space | O(n) | The depth map may store information for many nested directories |
The algorithm processes every line exactly once and performs only constant-time work per character. No repeated path reconstruction occurs, which avoids quadratic behavior.
The auxiliary space comes from the depth mapping. In the worst case, the filesystem may contain deeply nested directories, requiring storage proportional to the input size.
Test Cases
solution = Solution()
# Provided examples
assert solution.lengthLongestPath(
"dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext"
) == 20 # basic nested file
assert solution.lengthLongestPath(
"dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n"
"\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"
) == 32 # deeper nested longest file
assert solution.lengthLongestPath("a") == 0 # no files present
# Single file at root
assert solution.lengthLongestPath("file.txt") == 8 # root-level file
# Multiple root files
assert solution.lengthLongestPath(
"a.txt\nlongfilename.ext"
) == 16 # choose longest root file
# Deep nesting
assert solution.lengthLongestPath(
"dir\n\tsub1\n\t\tsub2\n\t\t\tsub3\n\t\t\t\tfile.ext"
) == 27 # deeply nested structure
# Directory names with spaces
assert solution.lengthLongestPath(
"my dir\n\tsub dir\n\t\tfile name.ext"
) == 32 # spaces inside names
# Multiple sibling directories
assert solution.lengthLongestPath(
"dir\n\ta\n\t\tfile1.txt\n\tb\n\t\tfile2.txt"
) == 15 # switching between sibling branches
# File directly under root directory
assert solution.lengthLongestPath(
"dir\n\tfile.txt"
) == 12 # simple parent-child case
# Longest file not deepest
assert solution.lengthLongestPath(
"dir\n\tshort\n\t\tf.ext\n\tverylongdirectory\n\t\ta.ext"
) == 29 # longest path determined by directory length
Test Summary
| Test | Why |
|---|---|
| Basic nested file | Verifies standard hierarchy parsing |
| Deep nested example | Validates multi-level directory traversal |
| No files present | Ensures result is 0 when no files exist |
| Single root file | Confirms root-level file handling |
| Multiple root files | Ensures longest file is selected |
| Deep nesting | Tests many depth levels |
| Names with spaces | Confirms spaces are parsed correctly |
| Multiple sibling directories | Ensures depth transitions work properly |
| File directly under root | Verifies simple parent-child paths |
| Longest file not deepest | Confirms length calculation is correct |
Edge Cases
No Files in the Filesystem
A filesystem may contain only directories and no files at all. This is important because a naive implementation might incorrectly return the longest directory path instead of 0.
For example:
dir
subdir
Since no entry contains a dot, the correct answer is 0.
The implementation handles this correctly because max_length is updated only when a file is encountered.
Files at the Root Level
A file may appear directly at depth 0 without any parent directory.
For example:
file.txt
This case is easy to mishandle if the algorithm assumes every file has a parent directory.
The implementation avoids this issue because depth_lengths[0] is initialized to 0, allowing root-level file lengths to be computed naturally.
Moving Back Up the Directory Hierarchy
A common bug occurs when processing sibling directories after returning from deeper nesting levels.
For example:
dir
a
file1.txt
b
file2.txt
When processing b, the algorithm must ignore the previously nested path under a.
The implementation handles this correctly because path lengths are indexed by depth. When a new directory appears at the same depth, it overwrites the previous entry for that depth, ensuring future children use the correct parent path.