はじめに
「有給があと10日あるけど、どこで使えば一番長く休めるのか」を自動で計算する Web ツールを作りました。
有給パズル → https://yukyu-puzzle.pages.dev/
残りの有給日数を入れると、土日・祝日・振替休日・会社の休み(年末年始など)と組み合わせて、連休が最も長くなる有給の取り方を返します。たとえば 2026年9月末から1年間、土日休みで有給10日の場合はこうなります。
- 12/28(月)に1日取るだけで、12/26(土)〜1/3(日)の 9連休
- 有給10日で合計 50日 の連休(有給1日あたり4.5日休める計算)
「GWは何日に有給を入れればいいか」はよく話題になりますが、1年分をまとめて、しかも「繁忙期は取れない」「会社独自の休みがある」といった条件付きで考えるのは、手作業だと意外と面倒です。これは「予算付きで区間を選ぶ最適化問題」なので、動的計画法(DP)で解けます。
この記事では、次の3つを紹介します。
- 祝日を法律のルールから計算する方法
- 連休の最適化を DP に落とし込む方法と、評価関数でハマった話
- 総当たりとの突き合わせによる検証
問題設定
- 期間内の各日は「休日(土日・祝日・会社の休み)」か「勤務日」のどちらか
- 勤務日のうち有給を使った日は休みになる。ただし「有給を取れない日(繁忙期)」には使えない
- 連休 = 休日と有給だけでできていて、前後を勤務日に挟まれた連続区間
- 有給の合計が予算 K 日以内で、連休の「価値の総和」を最大化したい
「価値」の定義は後で説明します。ここが一番ハマったところです。
祝日の計算
祝日は API から取ってくる方法もありますが、祝日法のルールで計算できるので、外部依存なしで実装しました。
- 固定日(元日、建国記念の日、天皇誕生日など)
- ハッピーマンデー(成人の日 = 1月の第2月曜、など)
- 春分の日・秋分の日(天文計算の近似式)
- 振替休日: 祝日が日曜なら、その後の最初の「祝日でない日」が休み
- 国民の休日: 前日と翌日が祝日である平日は休み(2026年の 9/22 がこれ)
春分・秋分は次の近似式で、1980〜2099年の範囲で使えます。
// 春分: base = 20.8431, 秋分: base = 23.2488
function equinoxDay(y, base) {
const k = y - 1980;
return Math.floor(base + 0.242194 * k - Math.floor(k / 4));
}
振替休日と国民の休日は、基本の祝日を並べた後に後処理で足しています。
// 国民の休日: 前日と翌日が祝日である平日
for (const n of base.keys()) {
if (base.has(n + 2) && !base.has(n + 1) && parts(n + 1).w !== 0) all.set(n + 1, '国民の休日');
}
// 振替休日: 日曜の祝日の後、最初の祝日でない日
for (const n of base.keys()) {
if (parts(n).w !== 0) continue;
let s = n + 1;
while (all.has(s)) s++;
all.set(s, '振替休日');
}
日付はすべて「1970-01-01 からの通算日数(UTC)」の整数で持っています。Date のままだとタイムゾーンの影響を受けるので、整数にしておくと隣の日が n + 1 で済んで楽です。
計算結果は、内閣府が公表している2026年の祝日(振替休日・国民の休日を含む18日)と一致することを確認しました。
DP の設計
状態と遷移
日を先頭から見ていく DP です。
- 状態
(i, k): 日 i より前は決定済みで、有給を k 日使った。日 i-1 は連休に含まれない - 遷移は2種類
- 日 i を連休に使わず、
(i+1, k)へ進む - 日 i から日 j までを連休にする。区間内の勤務日の数を c として、
(j+2, k+c)へ進む。日 j+1 は連休の後ろの勤務日なので読み飛ばす
- 日 i を連休に使わず、
ポイントは、連休を「極大な区間」に限定することです。連休は勤務日の直後から始まり、翌日が勤務日の日で終わるものだけを候補にします。こうしないと、土曜から数えた5連休と日曜から数えた4連休のような「同じ休みの数え方違い」が混ざって、価値を正しく計算できません。
for (let i = 0; i < N; i++) {
for (let k = 0; k <= K; k++) {
const cur = score[i * W + k];
if (cur === NEG) continue;
relax(i + 1, k, cur, i * W + k, -1, -1); // 日 i は連休にしない
if (i > 0 && !isWork[i - 1]) continue; // 連休は勤務日の直後から
let cost = 0;
for (let j = i; j < N && j - i < span; j++) {
if (isWork[j]) {
if (days[j].blocked) break; // 有給を取れない日
if (k + ++cost > K) break; // 予算オーバー
}
const L = j - i + 1;
if (cost > 0 && L > o.maxLen) break;
if (j + 1 < N && !isWork[j + 1]) continue; // 連休の翌日は勤務日
if (cost > 0 && L < o.minLen) continue;
relax(Math.min(N, j + 2), k + cost, cur + value[L], i * W + k, i, j);
}
}
}
計算量は O(期間の日数 × 有給日数 × 連休の最大長) です。1年・有給10日なら、ブラウザ上で10ms以下で終わります。入力のたびに再計算しても気になりません。
評価関数でハマった話
最初は、価値を次のように定義していました。
長さ L の連休の価値 = L^α (ただし L が「最低日数」未満なら 0)
これで「4連休以上だけを数える」ようにしたところ、結果が4連休ばかりになりました。
原因は、3連休の前後に有給を1日足して4連休にすると、価値が 0 から 4^α に跳ね上がることです。一方で、有給1日で年末年始の6連休を9連休に伸ばしても、増える価値は 9^α − 6^α しかありません。しきい値をまたぐことが過大評価されていました。
そこで、次のように変えました。
- 普通の土日を含めたすべての休みに L^α の価値を与える
- α > 1 にする。すると、休みと休みを有給で「つなげる」ほど価値が上がる
- 最低日数以上の連休には、別途ボーナスを足す
α = 1 では、有給をどこに置いても休みの合計日数は変わらないので、置き場所の好みが生まれません。α > 1 にすると、2連休と6連休を有給1日でつなぐ(2^α + 6^α → 9^α)のが得になり、直感どおり「年末年始の9連休」が選ばれるようになりました。
「休み方」の選択肢は、この α とボーナスの組み合わせで表しています。
| 休み方 | α | ボーナス | 傾向 |
|---|---|---|---|
| 大型連休重視 | 1.7 | 0 | 長い連休を少数 |
| バランス | 1.3 | 2 | その中間 |
| こまめに休む | 1.1 | 8 | 4連休を多数 |
同じ有給10日でも、「大型連休重視」なら 9連休・16連休・9連休の3回、「こまめに休む」なら4〜6連休を12回と、はっきり違う結果になります。
検証
最適化のコードは「それっぽい結果」が出ても間違っていることがあるので、2つの方法で確認しました。
- 総当たりとの比較: 2027年4月20日〜5月20日(勤務日は19日)について、勤務日の選び方をすべて(2^19 = 約52万通り)列挙し、有給4日以内の組み合わせの中での最良値を求めました。最良の価値が DP の結果と一致することを、α とボーナスの5通りの組み合わせで確認しています
- 構造のチェック: 出てきた各連休が、本当に休日と有給だけでできているか、前後が勤務日になっているか(極大か)を確認しました
テストはブラウザの外でも動くように、計算部分を planner.js という純粋な関数だけのファイルに分けています。
構成
- 静的な HTML と JavaScript だけで、サーバーはありません。入力内容はどこにも送信されません
- ホスティングは Cloudflare Pages の無料枠です
- 入力条件は URL のクエリに入れているので、結果をそのままリンクで共有できます
- 年末年始や GW など、時期ごとの解説ページも用意しました。これらも同じ
planner.jsの祝日計算から生成しているので、日付を手で書いていません
おわりに
「有給をどこで使うか」は毎年誰もが悩むわりに、条件を全部入れて最適化してくれる道具は意外と見当たらなかったので作ってみました。シフト勤務(休みの曜日を自由に指定できます)や繁忙期のある職場でも使えます。
「こういう条件も入れたい」などのフィードバックをもらえると嬉しいです。

