公式

G - 221 Subsequence 解説 by sheyasutaka


考察

\(A\) の任意の部分列 \(B = (A_{t_1}, \dots, A_{t_k})\) について,その添字列 \((t_1, \dots t_k)\) としてあり得る辞書順最小のものをとります.これは,\(B\) の先頭要素から順に,添字を貪欲に最小化したもの \(\displaystyle t'_i = \min_{u > t'_{i-1},\ A_{u} = A_{t_i}} u \) に一致します(証明略).これを \(B\)貪欲な添字列と呼ぶことにします.

221 数列であるような部分列に対応する貪欲な添字列 \(T = (t_1, \dots, t_k)\) について,「\(p = k\) または (\(p < k\) かつ \(A_{t_p} \neq A_{t_{p+1}}\))」を満たす位置 \(p\) を並べた添字列 \(P = (p_1, \dots, p_m)\) をおきます.\(P\) の要素は口語的には「\(B\) の貪欲な添字列のなかで,同じ要素値が続くカタマリの最後にあたる添字」と説明できます.このとき,\(i = 1, \dots, m\) に対して以下が成り立ちます.

  • \(A_{p_{i-1}} \neq A_{p_i}\) である(ここで,番兵 \(A_{p_0} := 0\) をおく).
  • 連続する要素 \(A_{p_{i-1} + 1},\ \cdots,\ A_{p_i}\) のうち,\(A_{p_i}\) と等しいものはちょうど \(A_{p_i}\) 個ある(ここで,番兵 \(p_0 := 0\) をおく).

逆に,任意の \(i = 1, \dots, |P|\) で上の \(2\) 条件を満たすような添字列 \(P\) に対し,対応する \(T\) ひいては \(B\) が一意に復元できます.よって,これを満たすような空でない添字列 \(P\) の個数が答えです.

\(A_1, \dots, A_{t}\) のうち \(A_t\) に等しいものの個数を \(C_t\) とおきます.また \(J(x,c)\) を,以下を満たす値として定めます.

  • \(c > 0\) の場合,\(A_{J(x,c)} = x\) かつ \(C_{J(x,c)} = c\)
  • \(c = 0\) の場合,\(J(x,c) = -1\)

このとき,点 \(i = 1, \dots, |P|\) における上の条件は,以下のように言い換えられます.

  • \(1 \leq p_i \leq N\) である.
  • \(0 \leq C_{p_i} - A_{p_i}\) である.
  • \(J(A_{p_i},\ C_{p_i} - A_{p_i} + 0) < p_{i-1} < J(A_{p_i},\ C_{p_i} - A_{p_i} + 1)\) である.

上の条件は \(p_{i-1}\) の値のみに依存し,また条件を満たす \(p_{i-1}\) は連続する範囲をなします.

\(dp[p]\) を,末尾が \(p\) であるような valid な \(P\) の個数として定めます.これを \(p = 1, \dots, N\) の昇順に求めることを考えます.なお,番兵として \(dp[0] := 1\) をおきます.

このとき \(dp[p]\) の値は,\(C_{p} - A_p < 0\) の場合は \(0\) であり,そうでない場合は上で求めた範囲の \(p_{i-1}\) に対する \(dp[p_{i-1}]\) の総和として得られます.よって,\(A_{p}, C_p\) の値および一点更新・区間和取得ができればよいです.

\(A_{p}, C_p\) の値は単純な前計算によって \(O(N)\) で得られ,一点更新・区間和取得はセグメント木によってクエリあたり \(O(\log N)\) で行えます(一点更新は \(p = 1, \dots, N\) の昇順に \(1\) 回ずつであることを利用し,累積和によって全体 \(O(N)\) で行うこともできます).

求める答えは \(dp[1], \dots, dp[N]\) の総和であり,以上を実装すれば求まります.

時間計算量は \(O(N \log N)\) (累積和で実装する場合は \(O(N)\)) となり,いずれにしても十分高速です.

実装例 (C++)

#include <iostream>
using std::cin;
using std::cout;
#include <string>
using std::string;
#include <algorithm>
#include <vector>
using std::vector;
#include <queue>
using std::queue;

typedef long long int ll;
const ll FOD = 998244353;

ll n;
vector<ll> a;


void solve () {
	vector<ll> precnt(n);
	vector<vector<ll> > idxs(n+1);

	vector<ll> dp(n);
	vector<ll> acc(n+1);
	acc[0] = 0;

	for (ll i = 0; i < n; i++) {
		precnt[i] = idxs[a[i]].size();
		idxs[a[i]].push_back(i);
	}

	for (ll i = 0; i < n; i++) {
		ll val;
		if (precnt[i] < a[i] - 1) {
			val = 0;
		} else if (precnt[i] == a[i] - 1) {
			// sum[0, idxs[a[i]][precnt[i] - a[i] + 1]) + 1
			val = FOD + acc[idxs[a[i]][precnt[i] - a[i] + 1]] - acc[0];
			val += 1;
		} else {
			// sum(idxs[a[i]][precnt[i] - a[i] + 0],
			//     idxs[a[i]][precnt[i] - a[i] + 1])
			val = FOD
				+ acc[idxs[a[i]][precnt[i] - a[i] + 1]]
				- acc[idxs[a[i]][precnt[i] - a[i] + 0] + 1];
		}
		val %= FOD;
		dp[i] = val;
		acc[i+1] = (acc[i] + dp[i]) % FOD;
	}

	ll ans = 0;
	for (ll i = 0; i < n; i++) {
		ans += dp[i];
	}
	ans %= FOD;
	cout << ans << "\n";
}

int main (void) {
	std::cin.tie(nullptr);
	std::ios_base::sync_with_stdio(false);

	cin >> n;
	a.resize(n);
	for (ll i = 0; i < n; i++) {
		cin >> a[i];
	}
	
	solve();

	return 0;
}

投稿日時:
最終更新: