[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\),第二輪總會有新死亡的起點。
如果我們將第一輪的最大偏移量作為邊界,求第二輪的變化範圍,則有:
\[l_{i,j} = min(0,l_{i,j}+e_j-l_{n,j}) \] \[r_{i,j} = max(0,r_{i,j}+e_j-r_{n,j}) \]這也不難理解,\(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\),那麼我們要算的事實上是:
\[\sum_{i = 1} ^ n\sum_{x =0}^T \prod_{j = 1}^m a_j - x\times b_j - g_{i,j} \]內層 \(\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;
}