Top

しゃくとり法 - ぺんぎんノート

しゃくとり法を使う問題は、2種類に分けられます。

  1. 範囲を広げていくと、いつか条件を満たさなくなる
  2. 範囲を広げていくと、いつか条件を満たす

たとえば、1.の問題には「ある数列の連続する部分列の和が$K$以下となるもののうち、最長の長さはいくつか」などがあり、2.の問題には「ある数列の連続する部分列の和が$K$以上となるもののうち、最も短い部分列の長さはいくつか」などがあります。

以下のテンプレートは、1.の問題に使えるものです。

T acc = e;  // 蓄積値。eは単位元のようなもの
for (int l = 0, r = 0; l < N; ) {
  while (r < N && /* accを更新しても条件を満たすか */) {
    /* accを更新してからr++ */
  }

  /* 区間[l, r)が条件を満たす範囲 */

  /* accを更新してからl++ */

  // この行は思考停止しながら書く
  if (l >= r) r = l, acc = e;
}

実装の方針としては、半開区間で実装しています。たとえば、上のテンプレートの7行目では、`acc`は区間$[l, r)$の値を保持しています。条件を満たす部分列の長さは$r-l$で取得できます。

続けて2.のテンプレートも載せます。

T acc = e;  // 蓄積値。eは単位元のようなもの
for (int l = 0, r = 0; l < N; ) {
  while (r < N && /* accを更新しても条件を満たさないか */) {
    /* accを更新してからr++ */
  }
  /* 条件を満たさない状態で最後までたどり着いた */
  if (r == N) break;

  /* 区間[l, r + 1)が条件を満たす範囲 */

  /* accを更新してからl++ */

  // この行は思考停止しながら書く
  if (l >= r) r = l, acc = e;
}

1.のテンプレートに`if (r == N) break;`が加わっただけです。あとは1.と同じ感覚で実装できます。ただ、ひとつ気をつけなければいけない点があり、9行目の`acc`は「ぎりぎり条件を満たさない値」を持っているという点です。ということで、条件を満たす値を作るために、`acc`と`r`を使う必要があります。

実際に出題された問題と、その実装

問題実装
AOJ Course The Number of Windows ソースコードへのリンク。`acc`は整数型、`e`は$0$とします。
POJ 3061 SubsequencePOJでは実装を公開できないので、ページ下に掲載します。
AtCoder ABC032 C - 列 ソースコードへのリンク。`acc`は整数型、`e`は$1$とします。
AtCoder ABC038 C - 単調増加 ソースコードへのリンク。`acc`は直前の数字、`e`は$0$とします。
AtCoder ARC022 B - 細長いお菓子 ソースコードへのリンク。`acc`は`set`、`e`は`{}`とします。
AtCoder ABC098 D - Xor Sum 2 ソースコードへのリンク。`acc`が複数あるパターンです。

POJ 3061 Subsequence以外の問題はすべて、1.のテンプレートに当てはめることができます。

#include <iostream>
#include <vector>

#define rep(i, n) for (int i = 0; i < int(n); i++)

using namespace std;

typedef long long i64;
typedef vector<i64> vi64;

int main() {
  int Q; cin >> Q;
  rep(i, Q) {
    int N, S; cin >> N >> S;
    vi64 a(N); rep(i, N) cin >> a[i];
    int ans = N + 1;
    i64 sum = 0;
    for (int l = 0, r = 0; l < N; ) {
      while (r < N && sum + a[r] < S) {
        sum += a[r++];
      }
      if (r == N) break;
      ans = min(ans, r - l + 1);
      sum -= a[l++];
      if (l >= r) r = l, sum = 0;
    }
    if (ans == N + 1) cout << 0 << endl;
    else cout << ans << endl;
  }
  return 0;
}

こちらがPOJ 3061 Subsequenceの実装です。`acc`は整数型、`e`は$0$です。2.の問題なので、`if (r == N) break;`という行があります。条件を満たす数列の長さは$r - l + 1$です。$r - l$でない理由は、$r - l$は「ぎりぎり条件を満たさない数列の長さ」だからです。$1$を足すことで、条件を満たす数列の長さになります。

まとめ

参考

しゃくとり法 (尺取り法) の解説と、それを用いる問題のまとめ