問題
短い文をPostFeed として
post(user, postId)は、人 userが番号 postIdの投稿を した ことを 覚えます。 投稿の 番号は どれも違います follow(a, b)は、人 aが人 bをフォローします。 すでに フォローしていれば 何もしません unfollow(a, b)は、人 aが人 bのフォローを 外します。 フォローしていなければ 何もしません getFeed(user)は、人 user自身と、userがいまフォローしている 人の 投稿から、 新しい 順に 最大 10 件の 投稿の 番号を 返します。 投稿が 1 つも 無ければ 空の並びです
自分follow(a, a) と unfollow(a, a) は
入力は、ops と、args でops のargs のpost、follow、unfollow は null です。
- 人の
番号は 1 以上 500 以下、 投稿の 番号は 0 以上 10000 以下です - 操作の
回数は 30000 回以下です
入力と出力の例
例 1
1 が 2 を
入力
ops = ["PostFeed", "post",
"post", "getFeed", "follow",
"post", "getFeed", "unfollow",
"getFeed"]
args = [[], [1, 101], [2, 201],
[1], [1, 2], [2, 202], [1],
[1, 2], [1]]出力
[null, null, null, [101], null,
null, [202, 201, 101], null,
[101]]例 2
1 は 2 を
入力
ops = ["PostFeed", "follow",
"post", "post", "post",
"post", "post", "post",
"post", "post", "post",
"post", "post", "post",
"getFeed", "getFeed"]
args = [[], [1, 2],
[1, 11], [2, 21], [1, 12],
[2, 22], [1, 13], [2, 23],
[1, 14], [2, 24], [1, 15],
[2, 25], [1, 16], [2, 26],
[1], [2]]出力
[null, null,
null, null, null,
null, null, null,
null, null, null,
null, null, null,
[26, 16, 25, 15, 24, 14,
23, 13, 22, 12],
[26, 25, 24, 23, 22, 21]]例 3
自分を
入力
ops = ["PostFeed", "post",
"follow", "getFeed",
"unfollow", "getFeed",
"getFeed"]
args = [[], [5, 50], [5, 5],
[5], [5, 5], [5], [9]]出力
[null, null, null, [50], null,
[50], []]面接での聞かれ方
テックリードの
- 「投稿、
フォロー、 フォロー解除、 タイムラインの 取得を 持つ SNS の クラスを 作ってください。 タイムラインは 新しい 順に 10 件」 - 「フォローしている
人が 1000 人いたら、 タイムラインは どう 作りますか」と、 全部の 投稿を 集めて 並べ替える 書き方の 重さを 聞かれます - 「自分を
フォローしたら?」 「自分を外したら?」と、 決まっていない 場合を 自分で 決めて 説明できるかを 見られます - 「投稿を
書いた 時点で、 フォロワーの タイムラインに 配っておく方法と どちらが よいですか」と、 読むときに 組む方法と 書く ときに 配る 方法の 取り 引きを 聞かれる こともあります
書き始める
アルゴリズムのチートシートの「ハッシュマップ」に、この型の要点を表とコードでまとめています。
まず 考える手順
段 1
素直には、
段 2
投稿のclock でrec と、last が
段 3
タイムラインは、
段 4
フォローは、follow(a, a) と unfollow(a, a) は
解答
Python
Python
from heapq import (
heapify, heappop, heappush,
)
def subs(sub, u):
# 初めてなら自分を入れて作る
if u not in sub:
sub[u] = {u}
return sub[u]
class PostFeed:
def __init__(self):
self.clock = 0
self.last = {}
self.rec = {}
self.sub = {}
def post(self, user, pid):
self.clock += 1
t = self.clock
last = self.last
prev = last.get(user, 0)
self.rec[t] = pid, prev
last[user] = t
def follow(self, a, b):
if a == b:
return
subs(self.sub, a).add(b)
def unfollow(self, a, b):
if a == b:
return
s = subs(self.sub, a)
s.discard(b)
def get_feed(self, user):
last = self.last
rec = self.rec
sub = self.sub
who = subs(sub, user)
h = []
for u in who:
t = last.get(u, 0)
if t:
h.append(-t)
heapify(h)
out = []
for _ in range(10):
if not h:
break
t = -heappop(h)
pid, prev = rec[t]
out.append(pid)
if prev == 0:
continue
heappush(h, -prev)
return outJavaScript
JavaScript
function heapPush(a, x) {
a.push(x);
let i = a.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (a[p] >= a[i]) break;
[a[p], a[i]] = [a[i], a[p]];
i = p;
}
}
function heapPop(a) {
const top = a[0];
const end = a.pop();
if (!a.length) return top;
a[0] = end;
let i = 0;
for (;;) {
const l = 2 * i + 1;
const r = l + 1;
let m = i;
if (a[l] > a[m]) m = l;
if (a[r] > a[m]) m = r;
if (m === i) return top;
[a[m], a[i]] = [a[i], a[m]];
i = m;
}
}
// 初めてなら自分を入れて作る
function subs(sub, u) {
if (!sub.has(u)) {
sub.set(u, new Set([u]));
}
return sub.get(u);
}
class PostFeed {
constructor() {
this.clock = 0;
this.last = {};
this.rec = {};
this.sub = new Map();
}
post(user, pid) {
this.clock += 1;
const t = this.clock;
const last = this.last;
const prev = last[user];
this.rec[t] = [pid, prev];
last[user] = t;
}
follow(a, b) {
if (a === b) return;
subs(this.sub, a).add(b);
}
unfollow(a, b) {
if (a === b) return;
const s = subs(this.sub, a);
s.delete(b);
}
getFeed(user) {
const { rec, sub } = this;
const who = subs(sub, user);
const h = [];
for (const u of who) {
const t = this.last[u];
if (t) heapPush(h, t);
}
const out = [];
while (out.length < 10) {
if (h.length === 0) break;
const t = heapPop(h);
const [id, prev] = rec[t];
out.push(id);
if (prev > 0) {
heapPush(h, prev);
}
}
return out;
}
}Go
Go
import "container/heap"
type maxHeap []int
func (h maxHeap) Len() int {
return len(h)
}
func (h maxHeap) Less(
i, j int,
) bool {
return h[i] > h[j]
}
func (h maxHeap) Swap(
i, j int,
) {
h[i], h[j] = h[j], h[i]
}
func (h *maxHeap) Push(x any) {
*h = append(*h, x.(int))
}
func (h *maxHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
type record struct {
id, prev int
}
type userSet map[int]bool
type PostFeed struct {
clock int
last map[int]int
rec map[int]record
sub map[int]userSet
}
func newPostFeed() *PostFeed {
return &PostFeed{
last: map[int]int{},
rec: map[int]record{},
sub: map[int]userSet{},
}
}
// 初めてなら自分を入れて作る
func subs(
sub map[int]userSet, u int,
) userSet {
s, ok := sub[u]
if !ok {
s = userSet{u: true}
sub[u] = s
}
return s
}
func (f *PostFeed) Post(
user, id int,
) {
f.clock++
t := f.clock
prev := f.last[user]
f.rec[t] = record{id, prev}
f.last[user] = t
}
func (f *PostFeed) Follow(
a, b int,
) {
if a != b {
subs(f.sub, a)[b] = true
}
}
func (f *PostFeed) Unfollow(
a, b int,
) {
if a != b {
delete(subs(f.sub, a), b)
}
}
func (f *PostFeed) GetFeed(
user int,
) []int {
h := &maxHeap{}
who := subs(f.sub, user)
for u := range who {
if t := f.last[u]; t > 0 {
*h = append(*h, t)
}
}
heap.Init(h)
out := []int{}
for h.Len() > 0 {
if len(out) == 10 {
break
}
t := heap.Pop(h).(int)
r := f.rec[t]
out = append(out, r.id)
if r.prev > 0 {
heap.Push(h, r.prev)
}
}
return out
}時計は 1 からundefined にprev > 0 がheapPop は、undefined にundefined > 数 は
計算量
post、follow、unfollowの時間は、 どれも 平均 O(1) です。 表に 1 つ 書き足すか、 集合に 足し引きするだけです getFeedの時間は O(k + 10 log k) です (k は 自分と フォロー先の 人数)。 集合を なめて ヒープを 作るのが O(k)、 そこから 10 回取り出して 入れ直すのが 1 回 O(log k) です。 全部の 投稿を 並べ替える 書き方の O(N log N) と 違い、 投稿の 総数 N に 関わりません - 使う
記憶は O(P + F) です (P は 投稿の 総数、 F は フォローの 関係の 総数)。 getFeedの中の ヒープは O(k) です
よく ある間違い
- 投稿の
番号で 新しさを決める。 番号は 投稿した 順とは 限りません。 例 1 で 202 が 101 より 新しいのは 番号が 大きいからではなく、 後に 投稿したからです。 仕組みの 中の 時計で決めます - 自分の
投稿を 入れ忘れる。 フォロー先だけを 集めると、 例 1 の 最初の getFeed(1)が空に なります。 逆に、 unfollow(a, a)で自分を 集合から 消せる 書き方に すると、 例 3 の 2 回目の getFeed(5)が空に なります。 自分は 集合に 最初から 入れ、 自分への 操作は 何もしないと決めます - 全部集めてから
並べ替える。 答えは 合いますが、 フォロー先が 多いと 1 回の getFeedで何万件も 並べ替えます。 面接では 「欲しいのは 10 件だけなので、 k 本の 新しい 順の 列を ヒープで まとめます」と 言えるかを 見られています - ヒープに
全部の 投稿を入れる。 全部入れてから 10 回取り出す 書き方は、 ヒープを 作るのに O(N) かかり、 各列の 先頭だけを 入れる 工夫が 生きません。 取り出した ときに、 同じ 人の 1 つ前だけを足します - フォローを
外した あとも、 外す前の 投稿を残す。 この 問題では、 外した 人の 投稿は 過去の 分も 消えます。 タイムラインを 毎回組み立て直す 書き方なら 自然に そうなりますが、 書く ときに フォロワーへ 配る 書き方では、 外した ときに 配った 分を 消す手間が かかります
解答を動かした版 Python 3.12.14Node.js 22.23.4Go 1.24.6確認日 2026年10月30日
面接での 話し方の例
書き始める
「新しさは
投稿の 番号ではなく、 仕組みの 中の 時計で 決めます。 人ごとに 最も 新しい 投稿の 時刻を 持ち、 各投稿には 同じ 人の 1 つ前の 投稿の 時刻を 持たせて、 新しい 順の 列にします。 タイムラインは、 自分と フォロー先の k 本の 列の 先頭だけを ヒープに 入れ、 取り出すたびに その 人の 1 つ前を 足す、を 10 回繰り返します。 時間は O(k + 10 log k) で、 投稿の 総数に 関わりません。 自分は 最初から フォローの 集合に 入れ、 自分への フォローと 解除は 何もしないと 決めます。」
書き終えたら、
「例 2 で、
1 の タイムラインの ヒープには、 1 の 最新の 16 と、 2 の 最新の 26 の 時刻が 入って 始まります。 26 の 時刻を 取り出すと 2 の 1 つ前の 25 の 時刻が 入り、 次は 16、 その次は 25、と 交互に 出て、 10 件目の 12 で 止まります。 フォロワーが 少なく 読む回数が 多いなら、 投稿の ときに フォロワーの タイムラインへ 配っておく方が 読むのは 速いですが、 フォロワーが 何百万人も いる 人の 投稿では 配る 手間が 重くなります。 そういう 人だけ 読むときに 組む、と 混ぜる 方法を 話します。」
