Skip to content
Merged
Show file tree
Hide file tree
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
200 changes: 200 additions & 0 deletions Algorithm/DynamicProgramming/other/Restore selected elements/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@

---

# 問題の概要:

与えられた配列 `A = [a₁, a₂, ..., aₙ]` から、**いくつかの要素を選んで和を `K` にする**。
ただし、**同じ位置の要素は1回まで使える(0-1制約)**。
和が作れない場合は `-1` を出力し、
作れる場合は**選んだ要素数が最小となる組み合わせ**を出力します。

---

# アルゴリズム:**動的計画法(0-1ナップサックDP)**

## DPテーブル定義

* `dp[i][k]`
**「最初のi個までの要素から選んで、和をkにするときの最小個数」**

* `prev[i][k]`
**「その状態に遷移する直前の和」**(復元用)

---

# 処理イメージ:図解

## 例題

```
A = [1, 3, 2, 2, 1]
K = 4
```

## DPテーブル構築(状態遷移)

### 初期化

```
dp[0][0] = 0 (和が0のときは0個選ぶ)
dp[0][k≠0] = ∞ (初期は不可能)
```

---

### 配列イメージ(i=0)

| k(和) | 0 | 1 | 2 | 3 | 4 |
| ---------- | - | - | - | - | - |
| dp\[0]\[k] | 0 | ∞ | ∞ | ∞ | ∞ |

---

### i=1(a₁=1)

* **使わない場合:** `dp[1][k] = dp[0][k]`
* **使う場合:** `dp[1][k+1] = min(dp[1][k+1], dp[0][k] + 1)`

| k(和) | 0 | 1 | 2 | 3 | 4 |
| ---------- | - | - | - | - | - |
| dp\[1]\[k] | 0 | 1 | ∞ | ∞ | ∞ |

---

### i=2(a₂=3)

* **使わない場合:** dpをそのまま
* **使う場合:**

```
dp[2][3] = min(dp[2][3], dp[1][0]+1) => 1個(3だけ選択)
dp[2][4] = min(dp[2][4], dp[1][1]+1) => 2個(1+3)
```

| k(和) | 0 | 1 | 2 | 3 | 4 |
| ---------- | - | - | - | - | - |
| dp\[2]\[k] | 0 | 1 | ∞ | 1 | 2 |

---

### i=3(a₃=2)

```
dp[3][2] = min(∞, dp[2][0]+1) => 1個(2)
dp[3][3] = min(1, dp[2][1]+1) => 1個(既にあるのでそのまま)
dp[3][4] = min(2, dp[2][2]+1) => 2個(2+2)
```

| k(和) | 0 | 1 | 2 | 3 | 4 |
| ---------- | - | - | - | - | - |
| dp\[3]\[k] | 0 | 1 | 1 | 1 | 2 |

---

### i=4(a₄=2)

* 同様に更新されるが、`dp[3][k]`が最適なので更新なし(同じ値が入る)

---

### i=5(a₅=1)

* 使わない場合:dp\[4]\[k]をコピー
* 使う場合:

```
dp[5][1] = min(1, dp[4][0]+1) => 1
dp[5][2] = min(1, dp[4][1]+1) => 1
dp[5][3] = min(1, dp[4][2]+1) => 1
dp[5][4] = min(2, dp[4][3]+1) => 2
```

---

# 最終DPテーブル

| k(和) | 0 | 1 | 2 | 3 | 4 |
| ---------- | - | - | - | - | - |
| dp\[5]\[k] | 0 | 1 | 1 | 1 | 2 |

---

# 復元処理(`prev`配列を使う)

* `dp[5][4]=2` →「和4を作るため、`prev[5][4]=3`」
* `dp[5][3]=1` →「和3を作るため、`prev[5][3]=1`」

### 復元した選択肢:

```
4 → 3 → 1 → 0
```

選んだ数:`3,1`
(順番は逆でも良い)

---

# 処理フロー全体

```plaintext
初期化 dp[0][0]=0

for i=1..N:
for k=0..K:
- 使わない場合 dp[i+1][k]=dp[i][k]
- 使う場合 dp[i+1][k+A[i]]=min(dp[i+1][k+A[i]], dp[i][k]+1)

復元(prev配列から逆追跡)

出力(個数 + 選んだ要素)
```

---

# 図まとめ(状態遷移)

```
i-1
|
┌──┴──┐
使わない 使う
↓ ↓
dp[i][k] dp[i][k+A[i]] = min(dp[i][k+A[i]], dp[i-1][k]+1)
```

---

# 計算量

| 項目 | 値 |
| ----- | -------- |
| 時間計算量 | O(N × K) |
| 空間計算量 | O(N × K) |

---

# メモリ・時間計測

```javascript
const startTime = process.hrtime.bigint();
...(処理)...
const endTime = process.hrtime.bigint();
console.error(`Time: ${(endTime - startTime) / 1e6} ms`);

const used = process.memoryUsage();
console.error(`Memory: ${Math.round(used.heapUsed / 1024)} KB`);
```

---

# まとめ

| 処理 | 内容 |
| ------ | --------------- |
| DP配列 | 和kを作る最小個数を保持 |
| prev配列 | 選んだ要素を復元するための履歴 |
| 復元処理 | 和Kから逆に辿る |
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// ご質問ありがとうございます。
// 「**同じ位置の数字は複数回使えない場合**」は、\*\*部分和問題の典型的な制約付き版(0-1ナップサック問題)\*\*になります。

// ---

// ## 問題の違い:

// | 条件 | 使える数字 |
// | ------ | -------------------------- |
// | **前回** | 同じ数字は何度でも使える(無限個) |
// | **今回** | 同じ\*\*位置の数字は1回だけ使える(0-1制約) |

// ---

// ## 解法:

// * 動的計画法(0-1ナップサック)
// * `dp[i][k]` :最初の i 個の数字から選んで和が k になるときの最小個数
// * **復元用に`prev`配列を持つ**

// ---

// ## コード(Node.js)

// ```javascript
const fs = require('fs');

/**
* 0-1制約付きの最小部分和問題
* @param {number} N - 数列の長さ
* @param {number} K - 目標の和
* @param {number[]} A - 数列A
* @returns {string} - 出力形式の文字列
*/
function minSubsetSum_01(N, K, A) {
const INF = 1e9;
const dp = Array.from({ length: N + 1 }, () => new Array(K + 1).fill(INF));
const prev = Array.from({ length: N + 1 }, () => new Array(K + 1).fill(-1)); // 復元用

dp[0][0] = 0;

for (let i = 0; i < N; i++) {
for (let k = 0; k <= K; k++) {
// i番目の要素を使わない場合
if (dp[i][k] < dp[i + 1][k]) {
dp[i + 1][k] = dp[i][k];
prev[i + 1][k] = k;
}
// i番目の要素を使う場合
if (k + A[i] <= K) {
if (dp[i][k] + 1 < dp[i + 1][k + A[i]]) {
dp[i + 1][k + A[i]] = dp[i][k] + 1;
prev[i + 1][k + A[i]] = k; // A[i]を使ってkからk+A[i]に遷移
}
}
}
}

if (dp[N][K] === INF) {
return '-1\n';
}

// 復元
const res = [];
let k = K;
for (let i = N; i >= 1; i--) {
if (prev[i][k] !== k) { // i番目の要素を使った場合
res.push(A[i - 1]);
k = prev[i][k];
}
}

return `${dp[N][K]}\n${res.join(' ')}\n`;
}

// 入力 & 計測
const input = fs.readFileSync('/dev/stdin', 'utf8').trim().split('\n');
const [N, K] = input[0].split(' ').map(Number);
const A = input[1].split(' ').map(Number);

const startTime = process.hrtime.bigint();
const result = minSubsetSum_01(N, K, A);
const endTime = process.hrtime.bigint();

process.stdout.write(result);

// パフォーマンス出力
const used = process.memoryUsage();
console.error(`Time: ${Number(endTime - startTime) / 1e6} ms`);
console.error(`Memory: ${Math.round(used.heapUsed / 1024)} KB`);
// ```

// ---

// ## 入力例と出力

// ### 入力

// ```
// 5 4
// 1 3 2 2 1
// ```

// ### 出力例

// ```
// 2
// 1 3
// ```

// ※順序は任意(`3 1`でもOK)

// ---

// ## 制約まとめ:

// * 各数字は**高々1回しか使えない**(同じ数字が複数あっても、同じ位置は1回だけ)
// * 解法は **0-1ナップサックDP**

// ---

// ## 計算量:

// * 時間計算量:O(N × K)
// * 空間計算量:O(N × K)

// ---

// ## メモリと実行時間:

// * `process.hrtime.bigint()`でナノ秒計測
// * `process.memoryUsage()`でヒープ使用量出力

Loading