cp-library

競技プログラミング用 C++ ライブラリ

View the Project on GitHub qandori/cp-library

:heavy_check_mark: Flat Hash Map

data_structure/flat_hash_map.hpp · GitHub で見る

共通ページ

キーに対応する値を追加・更新・削除する連想配列です。

#include "lib/data_structure/flat_hash_map.hpp"
#include <cstdint>

FlatHashMap<std::uint64_t, std::uint64_t> values;
values.reserve(200000);
values.set(10, 3);
values[20] += 5;
auto a = values.get(10); // optional(3)
auto b = values.get(30); // nullopt
if (auto *value = values.find(20)) *value += 2;
values.erase(10);
操作 内容・返り値
FlatHashMap<Key,T,Hash=std::hash<Key>,KeyEqual=std::equal_to<Key>>() 空のマップを作成
FlatHashMap(n) n 種類のキーを格納するための領域を確保
set(key,value) 値を追加または上書き
operator[](key) 値の T&。未登録なら T{} を追加
find(key) 値へのポインタ。未登録なら nullptr
get(key) 値を std::optional<T> で返す。未登録なら nullopt
at(key) 値への参照。未登録なら std::out_of_range
contains(key) / count(key) 存在判定/存在すれば 1、なければ 0
erase(key) キーを削除。存在すれば true
size() / empty() 登録されたキー数/空判定
prefetch(key) 後でアクセスするキーを先読みするヒント。内容は変更しない
reserve(n) n 種類のキーを格納するための領域を確保
clear() 全要素を削除
swap(other) 内容を交換

prefetch は const オブジェクトでも呼び出せる noexcept の操作で、未登録のキーも指定できます。環境によっては何も行いません。

find・at は const オブジェクトでは const T*・const T& を返します。取得したポインタや参照は、挿入・set・operator[]・reserve・削除・clear・代入の後には取得し直してください。既存キーの更新でも領域を確保し直すことがあります。

64 bit 以下の標準整数キーとコピー・既定構築・代入が例外を送出しない trivially copyable な値では、専用のハッシュテーブルを使います。その他の型や独自の Hash・KeyEqual では std::unordered_map を使います。Hash・KeyEqual は既定構築し、同値なキーのハッシュ値は等しくしてください。

get は値のコピー、operator[] は値の既定構築を必要とします。コピーは独立したマップを作り、移動元は再利用できる空のマップになります。size_type は std::size_t です。

prefetch は \(O(1)\) 時間です。検索・追加・更新・削除は期待償却 \(O(1)\) 時間です。確保した領域を \(C\) として、使用空間・コピー・clear は \(O(C)\)、reserve(m) は期待 \(O(C+m)\) 時間です。既に十分な領域があれば reserve は定数時間です。値・キー・ハッシュ・比較の演算を定数時間とします。

検証コード

コード

#ifndef FLAT_HASH_MAP_HPP
#define FLAT_HASH_MAP_HPP

#include <algorithm>
#include <bit>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>

namespace flat_hash_map_detail {
template<class K, class V, class Hash, class Equal>
inline constexpr bool packed = std::is_integral_v<K> && !std::is_same_v<K, bool> && sizeof(K) <= 8 &&
	std::is_same_v<Hash, std::hash<K>> && std::is_same_v<Equal, std::equal_to<K>> &&
	std::is_trivially_copyable_v<V> && std::is_nothrow_default_constructible_v<V> &&
	std::is_nothrow_copy_constructible_v<V> && std::is_nothrow_copy_assignable_v<V>;

template<class K, class V, class Hash, class Equal, bool = packed<K, V, Hash, Equal>>
class Table {
	std::unordered_map<K, V, Hash, Equal> values;
public:
	Table() = default;
	Table(const Table &) = default;
	Table(Table &&other) : Table() { swap(other); }
	Table &operator=(Table other) { swap(other); return *this; }
	void swap(Table &other) noexcept(noexcept(values.swap(other.values))) { values.swap(other.values); }
	std::size_t size() const { return values.size(); }
	bool empty() const { return values.empty(); }
	V *find(const K &key) { auto it = values.find(key); return it == values.end() ? nullptr : &it->second; }
	const V *find(const K &key) const { auto it = values.find(key); return it == values.end() ? nullptr : &it->second; }
	V &operator[](const K &key) { return values[key]; }
	void set(const K &key, const V &value) { values.insert_or_assign(key, value); }
	bool erase(const K &key) { return values.erase(key); }
	void prefetch(const K &) const noexcept {}
	void reserve(std::size_t n) { values.reserve(n); }
	void clear() { values.clear(); }
};

template<class K, class V, class Hash, class Equal>
class Table<K, V, Hash, Equal, true> {
	enum : unsigned char { vacant = 0, occupied = 1, tombstone = 2 };
	struct Entry { K key{}; V value{}; };
	struct ValueSlot { V value{}; };
	static constexpr bool combined = sizeof(Entry) == sizeof(K) + sizeof(V) && sizeof(V) <= 16;
	using Storage = std::conditional_t<combined, std::vector<Entry>, std::pair<std::vector<K>, std::vector<ValueSlot>>>;
	Storage storage;
	K &stored_key(std::size_t p) {
		if constexpr (combined) return storage[p].key;
		else return storage.first[p];
	}
	const K &stored_key(std::size_t p) const {
		if constexpr (combined) return storage[p].key;
		else return storage.first[p];
	}
	V &stored_value(std::size_t p) {
		if constexpr (combined) return storage[p].value;
		else return storage.second[p].value;
	}
	const V &stored_value(std::size_t p) const {
		if constexpr (combined) return storage[p].value;
		else return storage.second[p].value;
	}
	void resize(std::size_t capacity) {
		if constexpr (combined) storage.resize(capacity);
		else storage.first.resize(capacity), storage.second.resize(capacity);
	}
	std::size_t storage_limit() const {
		if constexpr (combined) return storage.max_size();
		else return std::min(storage.first.max_size(), storage.second.max_size());
	}
	std::vector<unsigned char> states;
	std::size_t count = 0, deleted = 0;
	unsigned shift = 64;
	std::uint64_t salt = std::chrono::steady_clock::now().time_since_epoch().count();
	std::size_t index(K key) const {
		return ((static_cast<std::uint64_t>(key) ^ salt) * UINT64_C(11400714819323198485)) >> shift;
	}
	std::size_t locate(K key) const {
		std::size_t p = index(key), mask = states.size() - 1;
		while (states[p] != vacant && (states[p] != occupied || stored_key(p) != key)) p = (p + 1) & mask;
		return p;
	}
	void rehash(std::size_t capacity) {
		Table next;
		next.salt = salt;
		next.resize(capacity), next.states.resize(capacity);
		next.shift = 64 - std::countr_zero(capacity);
		for (std::size_t i = 0; i < states.size(); ++i) if (states[i] == occupied) {
			std::size_t p = next.locate(stored_key(i));
			next.stored_key(p) = stored_key(i), next.stored_value(p) = stored_value(i), next.states[p] = occupied;
		}
		next.count = count;
		swap(next);
	}
	void prepare() {
		if (states.empty()) rehash(16);
		else if (count + deleted == states.size() / 2) {
			if (deleted > count / 2) rehash(states.size());
			else {
				if (states.size() > states.max_size() / 2 || states.size() > storage_limit() / 2)
					throw std::length_error("FlatHashMap capacity");
				rehash(states.size() * 2);
			}
		}
	}
	std::size_t insert(K key) {
		prepare();
		std::size_t p = index(key), first = states.size(), mask = states.size() - 1;
		while (states[p] != vacant) {
			if (states[p] == occupied && stored_key(p) == key) return p;
			if (states[p] == tombstone && first == states.size()) first = p;
			p = (p + 1) & mask;
		}
		if (first != states.size()) p = first, --deleted;
		stored_key(p) = key, stored_value(p) = V{}, states[p] = occupied;
		++count;
		return p;
	}
public:
	Table() = default;
	Table(const Table &) = default;
	Table(Table &&other) noexcept { swap(other); }
	Table &operator=(Table other) noexcept { swap(other); return *this; }
	void swap(Table &other) noexcept {
		storage.swap(other.storage), states.swap(other.states);
		std::swap(count, other.count), std::swap(deleted, other.deleted), std::swap(shift, other.shift), std::swap(salt, other.salt);
	}
	std::size_t size() const { return count; }
	bool empty() const { return !count; }
	V *find(K key) { if (states.empty()) return nullptr; std::size_t p = locate(key); return states[p] != vacant ? &stored_value(p) : nullptr; }
	const V *find(K key) const { if (states.empty()) return nullptr; std::size_t p = locate(key); return states[p] != vacant ? &stored_value(p) : nullptr; }
	V &operator[](K key) { return stored_value(insert(key)); }
	void set(K key, const V &value) { V copy = value; stored_value(insert(key)) = copy; }
	bool erase(K key) {
		if (states.empty()) return false;
		std::size_t p = locate(key);
		if (states[p] == vacant) return false;
		states[p] = tombstone, --count, ++deleted;
		return true;
	}
	void prefetch(K key) const noexcept {
#if defined(__GNUC__) || defined(__clang__)
		if (!states.empty()) {
			std::size_t p = index(key);
			__builtin_prefetch(states.data() + p, 0, 3);
			__builtin_prefetch(std::addressof(stored_key(p)), 0, 3);
			if constexpr (!combined) __builtin_prefetch(std::addressof(stored_value(p)), 0, 3);
		}
#else
		(void)key;
#endif
	}
	void reserve(std::size_t n) {
		if (n <= states.size() / 2) return;
		std::size_t limit = std::min(states.max_size(), storage_limit());
		if (n > limit / 2) throw std::length_error("FlatHashMap capacity");
		std::size_t capacity = std::bit_ceil(std::max(std::size_t(16), 2 * n));
		if (capacity > limit) throw std::length_error("FlatHashMap capacity");
		rehash(capacity);
	}
	void clear() { std::fill(states.begin(), states.end(), vacant); count = deleted = 0; }
};
}

template<class Key, class T, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>>
class FlatHashMap {
	flat_hash_map_detail::Table<Key, T, Hash, KeyEqual> table;
public:
	using key_type = Key;
	using mapped_type = T;
	using size_type = std::size_t;
	FlatHashMap() = default;
	explicit FlatHashMap(size_type n) { reserve(n); }
	bool empty() const { return table.empty(); }
	size_type size() const { return table.size(); }
	T *find(const Key &key) { return table.find(key); }
	const T *find(const Key &key) const { return table.find(key); }
	bool contains(const Key &key) const { return find(key) != nullptr; }
	size_type count(const Key &key) const { return contains(key); }
	std::optional<T> get(const Key &key) const { const T *p = find(key); return p ? std::optional<T>(*p) : std::nullopt; }
	T &at(const Key &key) { T *p = find(key); if (!p) throw std::out_of_range("FlatHashMap key"); return *p; }
	const T &at(const Key &key) const { const T *p = find(key); if (!p) throw std::out_of_range("FlatHashMap key"); return *p; }
	T &operator[](const Key &key) { return table[key]; }
	void set(const Key &key, const T &value) { table.set(key, value); }
	bool erase(const Key &key) { return table.erase(key); }
	void prefetch(const Key &key) const noexcept { table.prefetch(key); }
	void reserve(size_type n) { table.reserve(n); }
	void clear() { table.clear(); }
	void swap(FlatHashMap &other) noexcept(noexcept(table.swap(other.table))) { table.swap(other.table); }
	friend void swap(FlatHashMap &a, FlatHashMap &b) noexcept(noexcept(a.swap(b))) { a.swap(b); }
};

#endif
Back to top page