[NOIP2020] 微信步数
题目描述
小 C 喜欢跑步,并且非常喜欢在微信步数排行榜上刷榜,为此他制定了一个刷微信步数的计划。
他来到了一处空旷的场地,处于该场地中的人可以用 k k k 维整数坐标 ( a 1 , a 2 , … , a k ) (a_1, a_2, \ldots , a_k) (a1,a2,…,ak) 来表示其位置。场地有大小限制,第 i i i 维的大小为 w i w_i wi,因此处于场地中的人其坐标应满足 1 ≤ a i ≤ w i 1 \le a_i \le w_i 1≤ai≤wi( 1 ≤ i ≤ k 1 \le i \le k 1≤i≤k)。
小 C 打算在接下来的 P = w 1 × w 2 × ⋯ × w k P = w_1 \times w_2 \times \cdots \times w_k P=w1×w2×⋯×wk 天中,每天从场地中一个新的位置出发,开始他的刷步数计划(换句话说,他将会从场地中每个位置都出发一次进行计划)。
他的计划非常简单,每天按照事先规定好的路线行进,每天的路线由 n n n 步移动构成,每一步可以用 c i c_i ci 与 d i d_i di 表示:若他当前位于 ( a 1 , a 2 , … , a c i , … , a k ) (a_1, a_2, \ldots , a_{c_i}, \ldots, a_k) (a1,a2,…,aci,…,ak),则这一步他将会走到 ( a 1 , a 2 , … , a c i + d i , … , a k ) (a_1, a_2, \ldots , a_{c_i} + d_i, \ldots , a_k) (a1,a2,…,aci+di,…,ak),其中 1 ≤ c i ≤ k 1 \le c_i \le k 1≤ci≤k, d i ∈ { − 1 , 1 } d_i \in \{-1, 1\} di∈{−1,1}。小 C 将会不断重复这个路线,直到他走出了场地的范围才结束一天的计划。(即走完第 n n n 步后,若小 C 还在场内,他将回到第 1 1 1 步从头再走一遍)。
小 C 对自己的速度非常有自信,所以他并不在意具体耗费的时间,他只想知道 P P P 天之后,他一共刷出了多少步微信步数。请你帮他算一算。
输入格式
第一行两个用单个空格分隔的整数
n
,
k
n, k
n,k。分别表示路线步数与场地维数。
接下来一行
k
k
k 个用单个空格分隔的整数
w
i
w_i
wi,表示场地大小。
接下来
n
n
n 行每行两个用单个空格分隔的整数
c
i
,
d
i
c_i, d_i
ci,di,依次表示每一步的方向,具体意义见题目描述。
输出格式
仅一行一个整数表示答案。答案可能很大,你只需要输出其对
10
9
+
7
{10}^9 + 7
109+7 取模后的值。
若小 C 的计划会使得他在某一天在场地中永远走不出来,则输出一行一个整数
−
1
-1
−1。
样例 #1
样例输入 #1
3 2
3 3
1 1
2 -1
1 1
样例输出 #1
21
样例 #2
样例输入 #2
5 4
6 8 6 5
3 1
2 1
1 1
2 1
2 -1
样例输出 #2
10265
样例 #3
样例输入 #3
见附件中的 walk/walk3.in
样例输出 #3
见附件中的 walk/walk3.ans
样例 #4
样例输入 #4
见附件中的 walk/walk4.in
样例输出 #4
见附件中的 walk/walk4.ans
提示
【样例 #1 解释】
从
(
1
,
1
)
(1, 1)
(1,1) 出发将走
2
2
2 步,从
(
1
,
2
)
(1, 2)
(1,2) 出发将走
4
4
4 步,从
(
1
,
3
)
(1, 3)
(1,3) 出发将走
4
4
4 步。
从
(
2
,
1
)
(2, 1)
(2,1) 出发将走
2
2
2 步,从
(
2
,
2
)
(2, 2)
(2,2) 出发将走
3
3
3 步,从
(
2
,
3
)
(2, 3)
(2,3) 出发将走
3
3
3 步。
从
(
3
,
1
)
(3, 1)
(3,1) 出发将走
1
1
1 步,从
(
3
,
2
)
(3, 2)
(3,2) 出发将走
1
1
1 步,从
(
3
,
3
)
(3, 3)
(3,3) 出发将走
1
1
1 步。
共计
21
21
21 步。
【数据范围】
测试点编号 | n ≤ n \le n≤ | k ≤ k \le k≤ | w i ≤ w_i \le wi≤ |
---|---|---|---|
1 ∼ 3 1 \sim 3 1∼3 | 5 5 5 | 5 5 5 | 3 3 3 |
4 ∼ 6 4 \sim 6 4∼6 | 100 100 100 | 3 3 3 | 10 10 10 |
7 ∼ 8 7 \sim 8 7∼8 | 10 5 {10}^5 105 | 1 1 1 | 10 5 {10}^5 105 |
9 ∼ 12 9 \sim 12 9∼12 | 10 5 {10}^5 105 | 2 2 2 | 10 6 {10}^6 106 |
13 ∼ 16 13 \sim 16 13∼16 | 5 × 10 5 5 \times {10}^5 5×105 | 10 10 10 | 10 6 {10}^6 106 |
17 ∼ 20 17 \sim 20 17∼20 | 5 × 10 5 5 \times {10}^5 5×105 | 3 3 3 | 10 9 {10}^9 109 |
对于所有测试点,保证 1 ≤ n ≤ 5 × 10 5 1 \le n \le 5 \times {10}^5 1≤n≤5×105, 1 ≤ k ≤ 10 1 \le k \le 10 1≤k≤10, 1 ≤ w i ≤ 10 9 1 \le w_i \le {10}^9 1≤wi≤109, d i ∈ { − 1 , 1 } d_i \in \{-1, 1\} di∈{−1,1}。
称 nnn 步为一轮,首先 −1-1−1 的情况很好判断:一轮后回到原地且在第一轮里存在某个起点走不出去。
我们把要求的答案转换一下:原本是考虑每个起点各自走多少步出界,现在转换成同时考虑所有起点,把每天还 存活的起点 数量计入贡献。(这里存活就是指从该起点出发到某天还没出界)
显然,只要把第 000 天活着的起点算进去(也就是 ∏wi\prod w_i∏wi),就和要算的答案等价了。
一共 mmm 个维度,每个维度存活的位置是独立的,并且应是一段区间(只有开头、结尾的一部分会死亡)。
如果第 jjj 维存活的区间是 [lj,rj][l_j,r_j][lj,rj],那总共存活的数量就为 ∏j=1m(rj−lj+1)\prod\limits_{j=1}^m(r_j-l_j+1)j=1∏m(rj−lj+1) 。
根据这个想法,可以得到一个 O(nmT)O(nmT)O(nmT) 的算法,其中 TTT 最长轮数,最坏情况下为 max(wj)\max(w_j)max(wj) 。
以下代码实测可以拿 454545 分:
int w[20], e[20], l[20], r[20];
int c[N], d[N];
int n, m;
int main() {
scanf("%d%d", &n, &m);
LL ans = 1;
for (int i = 1; i <= m; i++) {
scanf("%d", &w[i]);
ans = ans * w[i] % mod;
}
for (int i = 1; i <= n; i++) {
scanf("%d%d", &c[i], &d[i]);
}
while (1) {
for (int i = 1; i <= n; i++) {
e[c[i]] += d[i];
l[c[i]] = min(l[c[i]], e[c[i]]);
r[c[i]] = max(r[c[i]], e[c[i]]);
LL s = 1;
for (int j = 1; j <= m; j++) {
if (r[j] - l[j] >= w[j]) goto M1;
s = s * (w[j] - r[j] + l[j]) % mod;
}
ans = (ans + s) % mod;
}
bool lose = 1;
for (int j = 1; j <= m; j++) {
if (e[j] != 0) lose = 0;
}
if (lose) {
ans = -1;
break;
}
}
M1:
printf("%lld\n", ans);
return 0;
}
下面考虑第 jjj 维:
在第一轮第 iii 步时,历史移动最大位移为 [li,ri][l_i,r_i][li,ri],那么死亡的起点数量应该为 ri−lir_i-l_iri−li 个。
这是因为 [1,−li][1,-l_i][1,−li] 和 [n−ri+1,n][n-r_i+1,n][n−ri+1,n] 范围内的起点已经死了。
假设第一轮总偏移量为 eje_jej,在第二轮第 iii 步时,历史移动最大位移应为 [min(li,ej+li),max(ri,ej+ri)][\min(l_i,e_j+l_i),\max(r_i,e_j+r_i)][min(li,ej+li),max(ri,ej+ri)]。
只要 ej≠0e_j\neq 0ej=0,无论 eje_jej 的正负,会有起点在第二轮是新死的。
把第一轮结束时的最大位移 [ln,rn][l_n,r_n][ln,rn] 作为边界,求第二轮第 iii 步时的左右扩张范围 [li′,ri′][l'_i,r'_i][li′,ri′],只需如下计算:
r[i][j] = max(0, r[i][j] + e[j] - r[n][j]);
l[i][j] = min(0, l[i][j] + e[j] - l[n][j]);
那么 ri′−li′r'_i-l'_iri′−li′ 就是第二轮中 1∼i1\sim i1∼i 步里新死的人。
容易发现一个事实:第 2,3,4⋯2,3,4\cdots2,3,4⋯ 轮里每步的死亡情况是一致的,只有第一轮是特殊的。(可以画个图理解下,除了第一轮外,其他轮都存在被上一轮已经扩张过的地方,死过的起点不会再死一次)
有了这个周期规律就可以优化了:
首先第一轮单独算,只考虑第二轮开始的。
设第一轮后还活着 aja_jaj 个起点,接下来每轮结束都有 bjb_jbj 个起点死亡,最后一轮的 1∼i1\sim i1∼i 步一共死了 fi=ri′−li′f_i=r'_i-l'_ifi=ri′−li′ 个点。
那么可以得到在 x+2x+2x+2 轮的第 iii 步时,第 jjj 维还活着 aj−x×bj−fia_j-x\times b_j-f_iaj−x×bj−fi 个点,贡献为 ∏j=1maj−x×bj−fi\prod\limits_{j=1}^m a_j-x\times b_j-f_ij=1∏maj−x×bj−fi。
设 T=minj=1maj−fibjT=\min\limits_{j=1}^m\frac{a_j-f_i}{b_j}T=j=1minmbjaj−fi,那么我们需要外层枚举 iii ,内层枚举 x=0,1,2,…,Tx=0,1,2,\ldots,Tx=0,1,2,…,T 。
(注意这里可能出现 aj−fi≤0a_j-f_i\le 0aj−fi≤0 的情况,说明第二轮这个维度的起点就死光了,那后面就不用算了)
如果老老实实这样枚举 xxx ,和之前做法就一样了。
要算的其实是 ∑i=1n∑x=0T∏j=1maj−x×bj−fi\sum\limits_{i=1}^n\sum\limits_{x=0}^T\prod\limits_{j=1}^m a_j-x\times b_j-f_ii=1∑nx=0∑Tj=1∏maj−x×bj−fi
内层 ∏\prod∏ 展开后,得到一个关于 xxx 的 mmm 次多项式 G(x)G(x)G(x),这个多项式系数可以暴力 O(m2)O(m^2)O(m2) 来算(我这里没有优化)
然后对多项式每项 pixkp_ix^kpixk,只要单独算 ∑x=0Txk\sum\limits_{x=0}^T x^kx=0∑Txk 即可。
关于计算 ∑i=1nik\sum\limits_{i=1}^n i^ki=1∑nik 参考 传送门。
而对本题而言, k≤3k\le 3k≤3 直接用公式,而 k>3k > 3k>3 时,nnn 不超过 10610^6106 直接预处理也可以。
时间复杂度 O(nm2)O(nm^2)O(nm2),代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 500005;
LL pow_mod(LL x, LL n) {
LL res = 1;
while (n) {
if (n & 1) res = res * x % mod;
n >>= 1;
x = x * x % mod;
}
return res;
}
LL fac[N];
// 计算 1^m+2^m+3^m+...+n^m
LL cal(LL n, LL m) {
LL res = 0;
if (n <= m + 2) {
for (int i = 1; i <= n; i++) {
res = (res + pow_mod(i, m)) % mod;
}
} else {
fac[0] = 1;
for (int i = 1; i <= m + 1; i++) {
fac[i] = fac[i - 1] * i % mod;
}
LL t = 1;
for (int i = 1; i <= m + 2; i++) {
t = t * (n - i) % mod;
}
LL y = 0;
int flag = (m + 2) % 2 ? 1 : -1;
for (int i = 1; i <= m + 2; i++) {
y = (y + pow_mod(i, m)) % mod;
res += flag * y * t % mod * pow_mod(n - i, mod - 2) % mod * pow_mod(fac[i - 1] * fac[m + 2 - i] % mod, mod - 2) % mod;
flag = -flag;
}
res = (res % mod + mod) % mod;
}
return res;
}
int w[20], e[20], l[N][20], r[N][20];
int a[20], b[20], h[20];
LL f[20][20];
int c[N], d[N];
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d", &w[i]);
}
for (int i = 1; i <= n; i++) {
scanf("%d%d", &c[i], &d[i]);
e[c[i]] += d[i];
for (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]]);
}
bool lose = 1;
for (int i = 1; i <= m; i++) {
if (e[i] != 0 || r[n][i] - l[n][i] >= w[i]) {
lose = 0;
}
}
if (lose) return puts("-1"), 0;
for (int j = 1; j <= m; j++) {
a[j] = w[j] - (r[n][j] - l[n][j]);
}
LL ans = 0;
// 第一轮贡献
for (int i = 0; i <= n; i++) {
LL s = 1;
for (int j = 1; j <= m; j++) {
s = s * max(0, (w[j] - (r[i][j] - l[i][j]))) % mod;
}
ans = (ans + s) % mod;
}
// 第二轮的死亡范围更新
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
r[i][j] = max(0, r[i][j] + e[j] - r[n][j]);
l[i][j] = min(0, l[i][j] + e[j] - l[n][j]);
}
}
for (int j = 1; j <= m; j++) {
b[j] = r[n][j] - l[n][j];
}
// 第二轮开始的贡献
int last = -1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) f[0][j] = 0;
f[0][0] = 1;
int t = INF;
for (int j = 1; j <= m; j++) {
int x = a[j] - r[i][j] + l[i][j];
if (x <= 0) goto M1; // 第二轮就暴毙了
if (b[j] > 0) t = min(t, x / b[j]);
for (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 != last) {
last = t;
for (int j = 1; j <= m; j++) h[j] = cal(t, j);
}
for (int j = 1; j <= m; j++) {
ans += h[j] * f[m][j] % mod;
}
}
M1:;
ans = (ans % mod + mod) % mod;
printf("%lld\n", ans);
return 0;
}