Skip to content
Closed
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
163 changes: 163 additions & 0 deletions algo/graph/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,81 @@
//! グラフの基本操作を提供するライブラリです。
//!
//! このライブラリは競技プログラミングでよく使われるグラフ操作を効率的に実行するための
//! 関数群を提供します。特に木の判定、連結性の確認、根付き木への変換などの基本的な
//! グラフアルゴリズムを含んでいます。
//!
//! # 主な機能
//!
//! - **木の判定**: グラフが木構造かどうかを O(E) で判定
//! - **連結性確認**: グラフが連結かどうかを DFS で確認
//! - **根付き木変換**: 無向グラフを指定した根を持つ根付き木に変換
//!
//! # 使用例
//!
//! ```
//! use graph::{is_tree, connectivity, tree_drop_parent};
//!
//! // 木の判定
//! let edges = vec![(0, 1), (1, 2), (2, 3)];
//! assert!(is_tree(4, &edges));
//!
//! // 連結性の確認
//! assert!(connectivity(4, &edges));
//!
//! // 根付き木への変換
//! let (children, parent) = tree_drop_parent(4, 0, &edges);
//! assert_eq!(parent, vec![0, 0, 1, 2]); // 各ノードの親
//! ```
//!
//! # 計算量
//!
//! - `is_tree`: O(E) (E: 辺数)
//! - `connectivity`: O(V + E) (V: 頂点数, E: 辺数)
//! - `tree_drop_parent`: O(V + E)

use std::mem;

/// グラフが木かどうかを判定します。
///
/// 木の条件:
/// - 連結である
/// - 辺の数が頂点数 - 1 である
/// - 閉路を持たない
///
/// # 引数
///
/// - `n`: 頂点数
/// - `edges`: 辺のリスト。各辺は `(u, v)` の形式で表現
///
/// # 戻り値
///
/// グラフが木の場合 `true`、そうでなければ `false`
///
/// # 計算量
///
/// O(E) (E: 辺数)
///
/// # Examples
///
/// ```
/// use graph::is_tree;
///
/// // 単純なパス: 0-1-2-3
/// let edges = vec![(0, 1), (1, 2), (2, 3)];
/// assert!(is_tree(4, &edges));
///
/// // 星型グラフ: 中央ノード0から1,2,3に接続
/// let edges = vec![(0, 1), (0, 2), (0, 3)];
/// assert!(is_tree(4, &edges));
///
/// // 閉路があるため木ではない
/// let edges = vec![(0, 1), (1, 2), (2, 0)];
/// assert!(!is_tree(3, &edges));
///
/// // 非連結のため木ではない
/// let edges = vec![(0, 1), (2, 3)];
/// assert!(!is_tree(4, &edges));
/// ```
pub fn is_tree(n: usize, edges: &[(usize, usize)]) -> bool {
for &(a, b) in edges {
assert!(a < n);
Expand All @@ -14,6 +90,40 @@ pub fn is_tree(n: usize, edges: &[(usize, usize)]) -> bool {
edges.len() == n - 1 && connectivity(n, edges)
}

/// グラフが連結かどうかを判定します。
///
/// DFS(深さ優先探索)を使用して、ノード0から全てのノードに到達できるかを確認します。
///
/// # 引数
///
/// - `n`: 頂点数
/// - `edges`: 辺のリスト。各辺は `(u, v)` の形式で表現
///
/// # 戻り値
///
/// グラフが連結の場合 `true`、そうでなければ `false`
///
/// # 計算量
///
/// O(V + E) (V: 頂点数, E: 辺数)
///
/// # Examples
///
/// ```
/// use graph::connectivity;
///
/// // 連結グラフ
/// let edges = vec![(0, 1), (1, 2), (2, 3)];
/// assert!(connectivity(4, &edges));
///
/// // 非連結グラフ(2つの成分に分かれている)
/// let edges = vec![(0, 1), (2, 3)];
/// assert!(!connectivity(4, &edges));
///
/// // 星型グラフ(連結)
/// let edges = vec![(0, 1), (0, 2), (0, 3), (0, 4)];
/// assert!(connectivity(5, &edges));
/// ```
pub fn connectivity(n: usize, edges: &[(usize, usize)]) -> bool {
fn dfs(i: usize, g: &[Vec<usize>], visited: &mut Vec<bool>) {
for &j in &g[i] {
Expand All @@ -36,6 +146,59 @@ pub fn connectivity(n: usize, edges: &[(usize, usize)]) -> bool {
visited.iter().filter(|&&f| f).count() == n
}

/// 無向グラフを根付き木に変換します。
///
/// 指定された根ノードを基準として、無向グラフを根付き木に変換し、
/// 各ノードの子ノードリストと親ノードを返します。
///
/// # 引数
///
/// - `n`: 頂点数
/// - `root`: 根ノードのインデックス
/// - `edges`: 辺のリスト。木構造を表現している必要があります
///
/// # 戻り値
///
/// タプル `(children, parent)`:
/// - `children`: 各ノードの子ノードのリスト
/// - `parent`: 各ノードの親ノード。根ノードの親は自分自身
///
/// # 計算量
///
/// O(V + E) (V: 頂点数, E: 辺数)
///
/// # パニック条件
///
/// 入力が木構造でない場合、debug モードでパニックします。
///
/// # Examples
///
/// ```
/// use graph::tree_drop_parent;
///
/// // 線形な木: 0-1-2-3 (根: 0)
/// let edges = vec![(0, 1), (1, 2), (2, 3)];
/// let (children, parent) = tree_drop_parent(4, 0, &edges);
/// assert_eq!(children, vec![vec![1], vec![2], vec![3], vec![]]);
/// assert_eq!(parent, vec![0, 0, 1, 2]);
///
/// // 星型グラフ: 中央ノード1が根
/// let edges = vec![(1, 0), (1, 2), (1, 3)];
/// let (children, parent) = tree_drop_parent(4, 1, &edges);
/// assert_eq!(children, vec![vec![], vec![0, 2, 3], vec![], vec![]]);
/// assert_eq!(parent, vec![1, 1, 1, 1]);
///
/// // 二分木の例
/// // 0
/// // / \
/// // 1 2
/// // /
/// // 3
/// let edges = vec![(0, 1), (0, 2), (1, 3)];
/// let (children, parent) = tree_drop_parent(4, 0, &edges);
/// assert_eq!(children, vec![vec![1, 2], vec![3], vec![], vec![]]);
/// assert_eq!(parent, vec![0, 0, 0, 1]);
/// ```
pub fn tree_drop_parent(
n: usize,
root: usize,
Expand Down
152 changes: 152 additions & 0 deletions algo/rolling_hash/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
//! Rolling Hash(ローリングハッシュ)による高速文字列処理ライブラリです。
//!
//! Rolling Hash は文字列の部分文字列のハッシュ値を O(1) で計算できる技法です。
//! 文字列の比較、パターンマッチング、部分文字列の検索などを高速化できます。
//!
//! # アルゴリズムの概要
//!
//! 文字列 S に対して、前計算として各位置までの累積ハッシュ値を計算しておくことで、
//! 任意の部分文字列 S[l..r] のハッシュ値を O(1) で求められます。
//!
//! # ハッシュ衝突について
//!
//! ハッシュ値が同じでも実際の文字列が異なる場合(ハッシュ衝突)があります。
//! このライブラリでは 2^61-1 を法とする大きな法を使用して衝突確率を下げていますが、
//! 完全には回避できません。競技プログラミングでは通常問題ありませんが、
//! 重要な用途では複数のハッシュ関数を併用することを推奨します。
//!
//! # 主な機能
//!
//! - **前計算**: O(n) で文字列全体のハッシュ値を計算
//! - **部分文字列ハッシュ**: O(1) で任意の部分文字列のハッシュ値を取得
//! - **部分文字列判定**: ある文字列が別の文字列の部分文字列かを高速判定
//!
//! # 使用例
//!
//! ```
//! use rolling_hash::RollingHash;
//!
//! let text = RollingHash::from_iter("abcdefg".bytes());
//! let pattern = RollingHash::from_iter("cde".bytes());
//!
//! // 部分文字列のハッシュ値を比較
//! assert_eq!(pattern.hash(0..3), text.hash(2..5)); // "cde" == "cde"
//!
//! // 部分文字列判定
//! assert!(pattern.is_substring(&text));
//! ```
//!
//! # 競技プログラミングでの応用
//!
//! - **文字列検索**: KMP法などの代替として高速な文字列検索
//! - **回文判定**: 文字列とその逆向きのハッシュ値を比較
//! - **最長共通部分文字列**: 複数文字列間での共通部分の効率的な検出
//! - **文字列の一意性判定**: 重複する部分文字列の検出
//!
//! # 計算量
//!
//! - 前計算: O(n) (n: 文字列長)
//! - 部分文字列ハッシュ取得: O(1)
//! - 部分文字列判定: O(m) (m: 検索対象文字列長)
//! - 空間計算量: O(n)

use std::{iter::FromIterator, ops};

const MASK30: u64 = (1 << 30) - 1;
Expand All @@ -10,6 +62,17 @@ const BASE: u64 = 1_000_000_000 + 9;
/// Rolling Hash です。O(文字列長) の前計算をしたうえで、部分文字列のハッシュ値を O(1) で計算します。
///
/// [実装の参考資料](https://qiita.com/keymoon/items/11fac5627672a6d6a9f6)
///
/// # Examples
///
/// 基本的な使用方法:
/// ```
/// use rolling_hash::RollingHash;
///
/// let rh = RollingHash::from_iter("hello".bytes());
/// let hash_full = rh.hash(0..5); // "hello" 全体
/// let hash_part = rh.hash(1..4); // "ell" 部分
/// ```
#[derive(Debug, Clone)]
pub struct RollingHash {
xs: Vec<u64>,
Expand All @@ -28,6 +91,31 @@ where
}

impl RollingHash {
/// 数値配列から Rolling Hash を構築します。
///
/// # 引数
///
/// - `xs`: ハッシュ化する数値の配列。通常は文字のバイト値
///
/// # 戻り値
///
/// 構築された `RollingHash` インスタンス
///
/// # 計算量
///
/// O(n) (n = `xs.len()`)
///
/// # Examples
///
/// ```
/// use rolling_hash::RollingHash;
///
/// // 文字列から構築
/// let rh1 = RollingHash::new(&[65, 66, 67]); // "ABC"
///
/// // または FromIterator を使用
/// let rh2 = RollingHash::from_iter("ABC".bytes());
/// ```
pub fn new(xs: &[u64]) -> Self {
let n = xs.len();
let xs = xs.to_vec();
Expand All @@ -42,20 +130,57 @@ impl RollingHash {
Self { xs, hashes, pows }
}

/// 文字列の長さを返します。
pub fn len(&self) -> usize {
self.xs.len()
}

/// 文字列が空かどうかを返します。
pub fn is_empty(&self) -> bool {
self.xs.is_empty()
}

/// 指定位置の文字(数値)を返します。
///
/// # 引数
///
/// - `i`: 取得する位置のインデックス
///
/// # パニック条件
///
/// `i >= self.len()` の場合にパニックします。
pub fn at(&self, i: usize) -> u64 {
assert!(i < self.len());
self.xs[i]
}

/// 部分文字列のハッシュ値を返します。
///
/// 指定された範囲の部分文字列のハッシュ値を O(1) で計算します。
///
/// # 引数
///
/// - `range`: 部分文字列の範囲(`start..end` 形式)
///
/// # 戻り値
///
/// 部分文字列のハッシュ値
///
/// # パニック条件
///
/// - `range.start > range.end` の場合
/// - `range.end > self.len()` の場合
///
/// # Examples
///
/// ```
/// use rolling_hash::RollingHash;
///
/// let rh = RollingHash::from_iter("hello".bytes());
/// let full_hash = rh.hash(0..5); // "hello"
/// let part_hash = rh.hash(1..4); // "ell"
/// let char_hash = rh.hash(0..1); // "h"
/// ```
pub fn hash(&self, range: ops::Range<usize>) -> u64 {
let l = range.start;
let r = range.end;
Expand All @@ -70,14 +195,41 @@ impl RollingHash {

/// self が other の部分文字列かどうかを返します。
///
/// ハッシュ値の比較により、self の文字列が other の文字列の
/// 部分文字列として含まれているかを判定します。
///
/// # 引数
///
/// - `other`: 検索対象となる文字列の `RollingHash`
///
/// # 戻り値
///
/// self が other の部分文字列の場合 `true`、そうでなければ `false`
///
/// # 計算量
///
/// O(other.len())
///
/// # 注意
///
/// ハッシュ値の一致により判定するため、極稀にハッシュ衝突による
/// 偽陽性(false positive)が発生する可能性があります。
///
/// # Examples
/// ```
/// use rolling_hash::RollingHash;
/// let rh1 = RollingHash::from_iter("abcd".bytes());
/// let rh2 = RollingHash::from_iter("xxabcdyy".bytes());
/// assert!(rh1.is_substring(&rh2));
///
/// // より実用的な例:パターン検索
/// let pattern = RollingHash::from_iter("world".bytes());
/// let text = RollingHash::from_iter("hello world!".bytes());
/// assert!(pattern.is_substring(&text));
///
/// // 存在しないパターン
/// let missing = RollingHash::from_iter("xyz".bytes());
/// assert!(!missing.is_substring(&text));
/// ```
// 出現位置をすべて返すようにしたほうがいいかも
pub fn is_substring(&self, other: &Self) -> bool {
Expand Down
Loading
Loading