题目大意
题目:KV 缓存
题目描述
Soy 正在运行一个大型语言模型(LLM)服务。为了避免为不同的请求重复计算相同的前缀,该服务维护了一个持久化的 KV 缓存。
每个请求由一个由小写拉丁字母组成的字符串表示,其中每个字母代表一个 token。具有公共前缀的请求可以共享 KV 缓存条目。我们将缓存的当前内容建模为一棵字典树(Trie)。
最初,这棵字典树只包含根节点,没有任何边。
在处理请求字符串 s s s 时,按顺序执行以下操作:
Soy 从左到右处理整个字符串,从字典树的根节点开始。
如果下一个 token 对应的边已经存在,则无需任何代价即可复用其缓存结果。
否则,Soy 必须计算相应的 KV 状态。这需要消耗 1 1 1 个单位的计算代价,并且缺失的边会被添加到字典树中。
整个请求在任何缓存条目被删除之前会被完整地处理。特别地,请求中所有缺失的边会首先被添加,即使这会暂时使字典树包含超过 m m m 条边。
在处理完整个请求后,Soy 会应用持久化缓存的大小限制。如果字典树包含超过 m m m 条边,他必须不断地删除一个叶子节点以及连接它与其父节点的边,直到字典树恰好剩下 m m m 条边。叶子节点是指在当前字典树中没有子节点的顶点。在处理当前请求时新添加的边也可以在此修剪步骤中被删除。
Soy 提前知道完整的 n n n 个请求序列。在每次修剪步骤中,他可以选择删除哪些叶子节点的边。
请计算处理所有 n n n 个请求所需的最小可能总计算代价。
字典树(Trie)是一种将一组字符串存储为有根树的数据结构。该树具有以下结构:树的每条边都标记有一个字母,同一个节点连出的边中,不存在两条标记相同字母的边。每个字符串可以通过沿着从根节点到某个顶点的路径来读取。
例如,我们可以为字符串 “min”、“trie”、“task” 和 “mini” 构建一棵字典树,它看起来像这样:
输入描述
每个测试用例的第一行包含两个整数 n n n 和 m m m (1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1 ≤ n ≤ 1 0 6 ,1 ≤ m ≤ 1 0 9 1 \le m \le 10^9 1 ≤ m ≤ 1 0 9 )。
接下来的 n n n 行中,第 i i i 行包含一个字符串 s i s_i s i —— 表示第 i i i 个请求的 token 序列。保证 s i s_i s i 仅由小写拉丁字母组成。保证所有字符串的长度之和 ∑ ∣ s i ∣ \sum \vert{}s_i\vert{} ∑ ∣ s i ∣ 不超过 1 0 6 10^6 1 0 6 。
输出描述
输出一个整数 —— 处理所有 n n n 个请求的最小总计算代价。
样例
在本样例中,缓存的边数限制为 m = 4 m = 4 m = 4 。一种最优的计算和修剪策略如下:
处理第一个请求 mini :依次添加边 m、i、n、i。计算代价为 4 4 4 。此时字典树有 4 4 4 条边,未超过限制,无需修剪。
处理第二个请求 trie :从根节点开始添加边 t、r、i、e。计算代价为 4 4 4 。此时字典树包含 8 8 8 条边,超出了 m = 4 m=4 m = 4 的限制,需要修剪掉 4 4 4 条边。我们可以选择删掉第一条链末尾的 i 以及第二条链后方的 r-i-e,只保留边 m-i-n 和 t(共计 4 4 4 条边)。
处理第三个请求 task :复用已有的边 t(代价为 0 0 0 ),然后依次添加边 a、s、k。计算代价为 3 3 3 。处理后字典树包含 7 7 7 条边,需要修剪掉 3 3 3 条边。我们选择修剪掉刚增加的 a-s-k,继续保留 m-i-n 和 t(共计 4 4 4 条边)。
处理第四个请求 min :边 m-i-n 已经完整存在于字典树中,可以直接复用全部 token,因此无需新建任何边。计算代价为 0 0 0 。此时树中有 4 4 4 条边,无需修剪。
所有请求的总计算代价为 4 + 4 + 3 + 0 = 11 4 + 4 + 3 + 0 = 11 4 + 4 + 3 + 0 = 1 1 。这也是在所有可能修剪策略中能达到的最小总计算代价。
思路讲解
问:题目相当于在每一次请求结束后,保留最多 m m m 条边,如何将其转化为经典的缓存问题?
问:在传统的离线场景下(已知所有未来的访问序列) ,最优的缓存替换策略 是什么?
问:直接对字典树的节点使用 Belady 策略,是否会违反“保留子节点必须保留父节点”的树形修剪约束?
问:如何高效模拟这一带有深度优先约束的贪心淘汰过程?
那么我们所谓的这个每个节点的数组啊,其实就是这个代码中的 visits 数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vector<string> s (n + 1 ) ;for (int i = 1 ; i <= n; ++i) { cin >> s[i]; int u = 1 ; for (char c : s[i]) { int idx = c - 'a' ; if (!ch[u][idx]) { ch[u][idx] = ++tot; depth[tot] = depth[u] + 1 ; } u = ch[u][idx]; visits[u].push_back (i); } }
然后我们删节点的话也不用真的去删除 啊,就用一个这个 in_cache 数组记一下在不在缓存 中就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (char c : s[i]) { int idx = c - 'a' ; u = ch[u][idx]; if (!in_cache[u]) { total_cost++; in_cache[u] = true ; cache_size++; } visit_ptr[u]++; int next_access = INF; if (visit_ptr[u] < visits[u].size ()) { next_access = visits[u][visit_ptr[u]]; } cur_next[u] = next_access; pq.push ({next_access, depth[u], u}); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 while (cache_size > m) { auto [na, d, v] = pq.top (); pq.pop (); if (na != cur_next[v]) { continue ; } if (!in_cache[v]) { continue ; } in_cache[v] = false ; cache_size--; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <iostream> #include <vector> #include <string> #include <queue> #include <tuple> using namespace std;const int INF = 1e9 ;const int MAXN = 1000005 ;int ch[MAXN][26 ];int depth[MAXN];int tot = 1 ; vector<int > visits[MAXN]; int visit_ptr[MAXN]; bool in_cache[MAXN]; int cur_next[MAXN]; int main () { ios_base::sync_with_stdio (false ); cin.tie (NULL ); int n, m; if (!(cin >> n >> m)) return 0 ; vector<string> s (n + 1 ) ; for (int i = 1 ; i <= n; ++i) { cin >> s[i]; int u = 1 ; for (char c : s[i]) { int idx = c - 'a' ; if (!ch[u][idx]) { ch[u][idx] = ++tot; depth[tot] = depth[u] + 1 ; } u = ch[u][idx]; visits[u].push_back (i); } } long long total_cost = 0 ; int cache_size = 0 ; priority_queue<tuple<int , int , int >> pq; for (int i = 1 ; i <= n; ++i) { int u = 1 ; for (char c : s[i]) { int idx = c - 'a' ; u = ch[u][idx]; if (!in_cache[u]) { total_cost++; in_cache[u] = true ; cache_size++; } visit_ptr[u]++; int next_access = INF; if (visit_ptr[u] < visits[u].size ()) { next_access = visits[u][visit_ptr[u]]; } cur_next[u] = next_access; pq.push ({next_access, depth[u], u}); } while (cache_size > m) { auto [na, d, v] = pq.top (); pq.pop (); if (na != cur_next[v]) { continue ; } if (!in_cache[v]) { continue ; } in_cache[v] = false ; cache_size--; } } cout << total_cost << "\n" ; return 0 ; }
AC代码
AC
https://ac.nowcoder.com/acm/contest/view-submission?submissionId=84468178
源代码(AI)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <iostream> #include <vector> #include <string> #include <queue> #include <tuple> using namespace std;const int INF = 1e9 ;const int MAXN = 1000005 ;int ch[MAXN][26 ];int depth[MAXN];int tot = 1 ; vector<int > visits[MAXN]; int visit_ptr[MAXN]; bool in_cache[MAXN]; int cur_next[MAXN]; int main () { ios_base::sync_with_stdio (false ); cin.tie (NULL ); int n, m; if (!(cin >> n >> m)) return 0 ; vector<string> s (n + 1 ) ; for (int i = 1 ; i <= n; ++i) { cin >> s[i]; int u = 1 ; for (char c : s[i]) { int idx = c - 'a' ; if (!ch[u][idx]) { ch[u][idx] = ++tot; depth[tot] = depth[u] + 1 ; } u = ch[u][idx]; visits[u].push_back (i); } } long long total_cost = 0 ; int cache_size = 0 ; priority_queue<tuple<int , int , int >> pq; for (int i = 1 ; i <= n; ++i) { int u = 1 ; for (char c : s[i]) { int idx = c - 'a' ; u = ch[u][idx]; if (!in_cache[u]) { total_cost++; in_cache[u] = true ; cache_size++; } visit_ptr[u]++; int next_access = INF; if (visit_ptr[u] < visits[u].size ()) { next_access = visits[u][visit_ptr[u]]; } cur_next[u] = next_access; pq.push ({next_access, depth[u], u}); } while (cache_size > m) { auto [na, d, v] = pq.top (); pq.pop (); if (na != cur_next[v]) { continue ; } if (!in_cache[v]) { continue ; } in_cache[v] = false ; cache_size--; } } cout << total_cost << "\n" ; return 0 ; }
AC
https://ac.nowcoder.com/acm/contest/view-submission?submissionId=84471363
我的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 #include <bits/stdc++.h> using namespace std;#define ll long long #define ld long double #define ui unsigned int #define ull unsigned long long #define i128 __int128_t #define endl '\n' #define all(x) (x).begin(), (x).end() #define rall(x) (x).rbegin(), (x).rend() const ll MOD = 0 , INF = 1e18 ;const ll MAXN = (ll)1e6 + 10 ;const char BASE = 'a' ;struct Con { ll u; ll nxt; ll dep; bool operator <(const Con &o) const { if (nxt != o.nxt) return nxt < o.nxt; return dep < o.dep; } }; void solve () { ll n, m; cin >> n >> m; vector<string> S (n) ; ll tot = 0 ; ll acc_len = 0 ; for (int i = 0 ; i < n; ++i) { cin >> S[i]; acc_len += S[i].size (); } vector<vector<int >> Trie (acc_len + 2 , vector <int >(26 )); vector<vector<int >> vis_tims (acc_len + 2 ); auto insert = [&](const string &ls, ll tim) { ll u = 0 ; for (auto c : ls) { if (Trie[u][c - BASE] == 0 ) { ++tot; Trie[u][c - BASE] = tot; u = tot; vis_tims[u].push_back (tim); } else { u = Trie[u][c - BASE]; vis_tims[u].push_back (tim); } } }; priority_queue<Con> pq; for (int i = 0 ; i < n; ++i) { insert (S[i], i); } vector<char > in_memo (tot + 2 ) ; ll ans = 0 ; ll curSZ = 0 ; auto insert_memo = [&](const string &ls, ll tim) -> void { ll u = 0 ; ll dep = 0 ; for (auto c : ls) { ll to = Trie[u][c - BASE]; dep++; if (!in_memo[to]) { in_memo[to] = true ; curSZ++; ++ans; } auto it = upper_bound (all (vis_tims[to]), tim); ll nxt = INF; if (it != vis_tims[to].end ()) { nxt = *it; } pq.push ({to, nxt, dep}); u = to; } }; auto pop_pq = [&](ll tim) { auto [u, nxt1, sz] = pq.top (); pq.pop (); ll nxt2 = INF; auto it = upper_bound (all (vis_tims[u]), tim); if (it != vis_tims[u].end ()) { nxt2 = *it; } if (nxt1 == nxt2) { in_memo[u] = false ; curSZ--; } }; for (int i = 0 ; i < n; ++i) { insert_memo (S[i], i); while (curSZ > m) { pop_pq (i); } } cout << ans << "\n" ; } int main () { cin.tie (0 ); cout.tie (0 ); ios::sync_with_stdio (0 ); solve (); return 0 ; }
心路历程(WA,TLE,MLE……)