Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions arai60/Tree_BT_BST/path-sum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# 112. Path Sum

LeetCode URL: https://leetcode.com/problems/path-sum/description/

この問題は Java で解いています。
各解法において、メソッドが属するクラスとして `Solution` を定義していますが、これは Java の言語仕様に従い、コードを実行可能にするために必要なものです。このクラス自体には特定の意味はなく、単にメソッドを組織化し、実行可能にするためのものです。

## Step 1

```java
// 解いた時間: 7分ぐらい
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックに入る
class Solution {
private record NodeWithPathSum(TreeNode node, int pathSum) {};

public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}

Deque<NodeWithPathSum> nodeStack = new ArrayDeque<>();
nodeStack.push(new NodeWithPathSum(root, 0));
while (!nodeStack.isEmpty()) {
NodeWithPathSum nodeWithPathSum = nodeStack.pop();
TreeNode node = nodeWithPathSum.node;
int currentPathSum = node.val + nodeWithPathSum.pathSum;

boolean isLeafNode = node.left == null && node.right == null;
if (isLeafNode && currentPathSum == targetSum) {
return true;
}

if (node.right != null) {
nodeStack.push(new NodeWithPathSum(node.right, currentPathSum));
}
if (node.left != null) {
nodeStack.push(new NodeWithPathSum(node.left, currentPathSum));
}
}

return false;
}
}
```

次のようなことを考えながら実装していました:

- DFS アプローチで解くのが適切に思える
- もっと効率よく出来ないか考えるも、全ノードを順にリーフノードまで見ていくのは避けられないと判断
- 値が0以上しか入り得ないなら途中で打ち切る処理も書けるが、負の数が入り得るのは constraints にも明記されてる
- 明記されてなくても、面接官に合意を取らない限りは打ち切るような処理は勝手に入れないほうがよさそう
- 値が0以上であるかどうかについては型システムによって保証されてるわけでもないので
- スタックオーバーフローのリスクを回避するためスタックで実装したい

## Step 2

### 再帰で DFS

スタックオーバーフローのリスクがあるので最適解にはならないと思いますが、一応練習も兼ねて書いてみます。

```java
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックフレームに積まれる
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
boolean isLeafNode = root.left == null && root.right == null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私はこれを変数には置かない気がします。ただ趣味の範囲です。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

返信遅くなりました。
確認するのに時間が空いたからか、改めて見ると冗長に感じたので、変数に置かないように step 4 を修正しました。

if (isLeafNode && root.val == targetSum) {
return true;
}
Comment on lines +71 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        if (isLeafNode) {
            return  root.val == targetSum;
        }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step 4 に修正版を書きました

return
hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
}
```

## Step 3

Step 1 と同じです。

```java
// 解いた時間: 5分ぐらい
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックに入る
class Solution {
private record NodeWithPathSum(TreeNode node, int pathSum) {};

public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}

Deque<NodeWithPathSum> nodeStack = new ArrayDeque<>();
nodeStack.push(new NodeWithPathSum(root, 0));
while (!nodeStack.isEmpty()) {
NodeWithPathSum nodeWithPathSum = nodeStack.pop();
TreeNode node = nodeWithPathSum.node;
int currentPathSum = node.val + nodeWithPathSum.pathSum;

boolean isLeafNode = node.left == null && node.right == null;
if (isLeafNode && currentPathSum == targetSum) {
return true;
}
Comment on lines +105 to +107
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上と同じく、葉ノードならここでリターンするのが自然な気がします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらの場合は currentPathSum == targetSum が false の場合は処理を継続しなければならないので、こんな感じでしょうか

            if (isLeafNode) {
                if (currentPathSum == targetSum) {
                    return true;
                }
                
                continue;
            }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうです!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!こちらも step 4 に記載しました


if (node.right != null) {
nodeStack.push(new NodeWithPathSum(node.right, currentPathSum));
}
if (node.left != null) {
nodeStack.push(new NodeWithPathSum(node.left, currentPathSum));
}
}

return false;
}
}
```

## Step 4

### イテレーティブな DFS

次の指摘に対応:

- https://github.com/seal-azarashi/leetcode/pull/24#discussion_r1781228402

```java
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックに入る
class Solution {
private record NodeWithPathSum(TreeNode node, int pathSum) {};

public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}

Deque<NodeWithPathSum> nodeStack = new ArrayDeque<>();
nodeStack.push(new NodeWithPathSum(root, 0));
while (!nodeStack.isEmpty()) {
NodeWithPathSum nodeWithPathSum = nodeStack.pop();
TreeNode node = nodeWithPathSum.node;
int currentPathSum = node.val + nodeWithPathSum.pathSum;

if (node.left == null && node.right == null) {
if (currentPathSum == targetSum) {
return true;
}

continue;
}

if (node.right != null) {
nodeStack.push(new NodeWithPathSum(node.right, currentPathSum));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java は最近(Tiger 以降)ほとんど書いていないのですが、record は null 入らないんでしたっけ。スタックには null も入るようにして、チェックを pop してから行うのも一つかと思いました。

Copy link
Copy Markdown
Owner Author

@seal-azarashi seal-azarashi Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

返信遅くなりました。
Null が入らないのは ArrayDeque の方になります。 Record はイミュータブルですが null を代入することは可能です。
確かに、別のクラスを使って、チェックを pop してから行うのも一つですが、経験上 null を許容しない設計になっているクラス (メンバ etc.) ないしコレクションの方が扱いやすかったので、こちらをあえて選んでいました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あれ、ArrayDeque は、null が入らないのはいいですが、null を要素に持つ Record も入らないということですか?
それとも、文法上はTreeNode node = nodeWithPathSum.node;の直後に null check をして、あと3箇所のチェックを消すというのも選択として可能ということですか。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、すいません読み違えてました。Null であるフィールドを持つ record は許容されます (null だとだめなのは record のインスタンスでした)。ですので、スタックには null も入るようにして、チェックを pop してから行うのは可能です。

こんな実装になります (step 4 に記載しました)

class Solution {
    private record NodeWithPathSum(TreeNode node, int pathSum) {};

    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }

        Deque<NodeWithPathSum> nodeStack = new ArrayDeque<>();
        nodeStack.push(new NodeWithPathSum(root, 0));
        while (!nodeStack.isEmpty()) {
            NodeWithPathSum nodeWithPathSum = nodeStack.pop();
            TreeNode node = nodeWithPathSum.node;

            if (node == null) {
                continue;
            }

            int currentPathSum = node.val + nodeWithPathSum.pathSum;
            if (node.left == null && node.right == null) {
                if (currentPathSum == targetSum) {
                    return true;
                }
                
                continue;
            }
            nodeStack.push(new NodeWithPathSum(node.right, currentPathSum));
            nodeStack.push(new NodeWithPathSum(node.left, currentPathSum));
        }
        return false;
    }
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらの方が見通しが良く感じました。

}
if (node.left != null) {
nodeStack.push(new NodeWithPathSum(node.left, currentPathSum));
}
}
return false;
}
}
```

スタックには null も入るようにして、チェックを pop してから行うパターン (参考: https://github.com/seal-azarashi/leetcode/pull/24#discussion_r1781230995)

```java
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックに入る
class Solution {
private record NodeWithPathSum(TreeNode node, int pathSum) {};

public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}

Deque<NodeWithPathSum> nodeStack = new ArrayDeque<>();
nodeStack.push(new NodeWithPathSum(root, 0));
while (!nodeStack.isEmpty()) {
NodeWithPathSum nodeWithPathSum = nodeStack.pop();
TreeNode node = nodeWithPathSum.node;

if (node == null) {
continue;
}

int currentPathSum = node.val + nodeWithPathSum.pathSum;
if (node.left == null && node.right == null) {
if (currentPathSum == targetSum) {
return true;
}

continue;
}
nodeStack.push(new NodeWithPathSum(node.right, currentPathSum));
nodeStack.push(new NodeWithPathSum(node.left, currentPathSum));
}
return false;
}
}
```

### 再帰で DFS

```java
// 時間計算量: O(n): 最大で全ノードを走査する
// 空間計算量: O(n): 最大で全ノードがスタックフレームに積まれる
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
return
hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
}
```