[NOIP2020]微信步数[题解]
微信步数
题目描述
小 C 喜欢跑步,并且非常喜欢在微信步数排行榜上刷榜,为此他制定了一个刷微信步数的计划。
他来到了一处空旷的场地,处于该场地中的人可以用 \(k\) 维整数坐标 \((a_1, a_2, \ldots , a_k)\) 来表示其位置。场地有大小限制,第 \(i\) 维的大小为 \(w_i\),因此处于场地中的人其坐标应满足 \(1 \le a_i \le w_i\)(\(1 \le i \le k\))。
小 C 打算在接下来的 \(P = w_1 \times w_2 \times \cdots \times w_k\) 天中,每天从场地中一个新的位置出发,开始他的刷步数计划(换句话说,他将会从场地中每个位置都出发一次进行计划)。
他的计划非常简单,每天按照事先规定好的路线行进,每天的路线由 \(n\) 步移动构成,每一步可以用 \(c_i\) 与 \(d_i\) 表示:若他当前位于 \((a_1, a_2, \ldots , a_{c_i}, \ldots, a_k)\),则这一步他将会走到 \((a_1, a_2, \ldots , a_{c_i} + d_i, \ldots , a_k)\),其中 \(1 \le c_i \le k\),\(d_i \in \{-1, 1\}\)。小 C 将会不断重复这个路线,直到他走出了场地的范围才结束一天的计划。(即走完第 \(n\) 步后,若小 C 还在场内,他将回到第 \(1\) 步从头再走一遍)。
小 C 对自己的速度非常有自信,所以他并不在意具体耗费的时间,他只想知道 \(P\) 天之后,他一共刷出了多少步微信步数。请你帮他算一算。
【数据范围】
测试点编号 | \(n \le\) | \(k \le\) | \(w_i \le\) |
---|---|---|---|
\(1 \sim 3\) | \(5\) | \(5\) | \(3\) |
\(4 \sim 6\) | \(100\) | \(3\) | \(10\) |
\(7 \sim 8\) | \({10}^5\) | \(1\) | \({10}^5\) |
\(9 \sim 12\) | \({10}^5\) | \(2\) | \({10}^6\) |
\(13 \sim 16\) | \(5 \times {10}^5\) | \(10\) | \({10}^6\) |
\(17 \sim 20\) | \(5 \times {10}^5\) | \(3\) | \({10}^9\) |
对于所有测试点,保证 \(1 \le n \le 5 \times {10}^5\),\(1 \le k \le 10\),\(1 \le w_i \le {10}^9\),\(d_i \in \{-1, 1\}\)。
分析
考虑将起点合并在一起移动,定义 \(l_i\) 为第 \(i\) 维向左的最大偏移量,\(r_i\) 为第 \(i\) 维向右的最大偏移量。
每次移动的贡献为本次移动前存活起点的数量。即 \(\prod_{i = 1}^k w_i - (r_i - l_i)\)。这是因为对于每一维来说,闭区间 \([1,-l_i]\) 和 \([n - r_i + 1, n]\) 范围内的起点已经被淘汰了,这不难理解。只需注意当某一维满足 \(r_i - l_i >= w_i\) 时,说明这一维上所有点对应的起点都已经死亡,对于我们来说,也意味着统计结束。
判断无解的情况亦不难,我们发现,只要 \(n\) 次移动后任意一维存在偏移量 且中途没有走出场地,即为无解。
暴力求解的复杂度是 \(O(Tnk)\) 的,其中 \(T\) 取决于移动的轮次,在这里,排除无解后至少会存在一维有 \(1\) 的偏移量,最坏情况下即需要 \(max_{1\leq i\leq k}\{w_i \}\) 轮。
而显然,这样的复杂度是不足以通过此题的,考虑在这个基础上进行优化。
注意到我们复杂度的瓶颈事实上主要在于轮次,考虑从这里入手。
在暴力程序的求解中,我们能够隐约感觉到每 \(n\) 次移动后存在着一定的周期性。事实上也正是如此。
略微扩充我们上面的定义,设 \(l_{i,j}\) 表示第一轮第 \(i\) 步第 \(j\) 维向左偏移的最大值,\(r_{i,j}\) 表示第一轮第 \(i\) 步第 \(j\) 维向右偏移的最大值。
设第一轮第 \(i\) 维总的偏移量为 \(e_i\),则对于第二轮的第 \(i\) 步,向左的最大偏移量事实上应该是 \(min(l_{i,j}, e_j + l_{i,j})\),向右的最大偏移量为 \(max(r_{i,j},e_j+r_{i,j})\)。
而只要 \(e_i \neq 0\),第二轮总会有新死亡的起点。
如果我们将第一轮的最大偏移量作为边界,求第二轮的变化范围,则有:
这也不难理解,\(e_j\) 相当于初始位置,而 \(l_{i,j}\) 相当于第二轮在第 \(i\) 步第 \(j\) 维的向左最大偏移量,减去 \(l_{n,i}\) 即算差值,即新增的死去起点。\(r_{i,j}\) 同理。
那么,现在的 \(r_{i,j} - l_{i,j}\),即第二轮中 \(1\) \(∼\) \(i\) 步第 \(j\) 维新死掉的起点。
而有一个显然的事实,除了第一轮外,之后的所有轮次每步的死亡情况是一致的,一个感性的理解是,其他轮都存在被上一轮扩张过的地方,死过的起点不会再死一次,只有第一轮不存在这种情况。
那么,我们单独计算第一轮,再考虑从第二轮开始的其他轮次。
设第一轮后第 \(i\) 维还存活 \(a_i\) 个起点,每轮结束第 \(i\) 维都有 \(b_i\) 个起点死亡,最后一轮 \(1\) \(∼\) \(i\) 步第 \(j\) 维一共死了 \(g_{i,j} = r_{i,j} - l_{i,j}\) 个起点。
那么,在第 \(x+2\) 轮的第 \(i\) 步,第 \(j\) 维还存活着 \(a_j - x\times b_j - g_{i,j}\) 个起点,贡献为 \(\prod_{j = 1} ^ m a_j - x\times b_j - g_{i,j}\)。
设 \(T = min_{1\leq j\leq m}\{\lfloor \frac{a_j-g_{i,j}}{b_j}\rfloor \}\),外层枚举 \(i\),内层枚举 $x = $ \(0\) \(∼\) \(T\),那么我们要算的事实上是:
内层 \(\prod\) 展开后,是一个关于 \(x\) 的 \(m\) 次多项式,这个多项式的系数我们可以通过 \(O(k^2)\) 暴力计算。之后对于多项式的每一个单项 \(p_i x ^k\),只需要计算 \(\sum_{x = 0} ^ T x^k\) 即可。
关于 \(\sum_{i = 1}^n i^k\),可以使用拉格朗日插值法求解,时间复杂度 \(O(k)\)。
总体时间复杂度 \(O(nk^2)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, M = 20, mod = 1e9 + 7, INF = 1e15;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m, ans;
int c[N], d[N];
int a[M], b[M], h[M], f[M][M];
int w[M], e[M], l[N][M], r[N][M];
int y[N], pre[N], suf[N], fac[N];
inline int power(int x, int k)
{
int res = 1;
while(k){
if(k & 1) res = res * x % mod;
k >>= 1, x = x * x % mod;
}
return res;
}
inline void initial()
{
fac[0] = 1;
for(register int i = 1; i <= N - 1; i++) fac[i] = fac[i - 1] * i % mod;
}
inline int Lagrange(int x, int z)
{
int res = 0;
pre[0] = 1, suf[z + 3] = 1;
for(register int i = 1; i <= z + 2; i++) pre[i] = pre[i - 1] * ((x - i + mod) % mod) % mod;
for(register int i = z + 2; i >= 1; i--) suf[i] = suf[i + 1] * ((x - i + mod) % mod) % mod;
for(register int i = 1; i <= z + 2; i++){
int a = pre[i - 1] * suf[i + 1] % mod, b = fac[i - 1] * fac[z + 2 - i] % mod;
if((z + 2 - i) % 2) b = mod - b;
res = res + y[i] * a % mod * power(b, mod - 2) % mod, res %= mod;
}
return res;
}
inline int calc(int a, int b)
{
for(register int i = 1; i <= b + 2; i++) y[i] = (y[i - 1] + power(i, b)) % mod;
return Lagrange(a, b);
}
signed main()
{
//freopen("P7116_17.in", "r", stdin);
initial();
n = read(), m = read(), ans = 1;
for(register int i = 1; i <= m; i++)
w[i] = read(), ans = ans * w[i] % mod;
for(register int i = 1; i <= n; i++){
c[i] = read(), d[i] = read(), e[c[i]] += d[i];
for(register int j = 1; j <= m; j++)
l[i][j] = l[i - 1][j], r[i][j] = r[i - 1][j];
l[i][c[i]] = min(l[i][c[i]], e[c[i]]);
r[i][c[i]] = max(r[i][c[i]], e[c[i]]);
int res = 1; //求取第一轮的贡献
for(register int j = 1; j <= m; j++)
res = res * max((int)0, (w[j] - (r[i][j] - l[i][j]))) % mod;
ans = (ans + res) % mod;
}
for(register int i = 1; i <= m; i++)
if(e[i] || r[n][i] - l[n][i] >= w[i]) goto NEX; //能够走出去
puts("-1"); return 0;
NEX:;
for(register int i = 1; i <= m; i++) a[i] = w[i] - (r[n][i] - l[n][i]); //第一轮留下的贡献
for(register int i = 1; i <= n; i++) //每一轮的每一次移动新死亡起点的个数
for(register int j = 1; j <= m; j++)
r[i][j] = max((int)0, r[i][j] + e[j] - r[n][j]), l[i][j] = min((int)0, l[i][j] + e[j] - l[n][j]); //第二轮范围更新
for(register int i = 1; i <= m; i++) b[i] = r[n][i] - l[n][i]; //此后每一轮偏移的数
for(register int i = 1, lst = -1; i <= n; i++){
for(register int j = 0; j <= m; j++) f[0][j] = 0;
f[0][0] = 1;
int t = INF;
for(register int j = 1; j <= m; j++){
int x = a[j] - (r[i][j] - l[i][j]);
if(x <= 0) goto END; //第二轮结束
if(b[j] > 0) t = min(t, x / b[j]);
for(register int k = 0; k <= m; k++){
f[j][k] = f[j - 1][k] * x % mod;
if(k > 0) f[j][k] = (f[j][k] + f[j - 1][k - 1] * -b[j]) % mod;
}
}
ans += f[m][0] * (t + 1) % mod;
if(t != lst){ //重新算系数
lst = t;
for(register int j = 1; j <= m; j++) h[j] = calc(t, j);
}
for(register int j = 1; j <= m; j++)
ans += h[j] * f[m][j] % mod, ans = (ans % mod + mod) % mod;
}
END:;
printf("%lld\n", ans);
return 0;
}