[NOIP2020] 微信步数

题目描述

小 C 喜欢跑步,并且非常喜欢在微信步数排行榜上刷榜,为此他制定了一个刷微信步数的计划。

他来到了一处空旷的场地,处于该场地中的人可以用 kk 维整数坐标 (a_1, a_2, \ldots , a_k)(a1​,a2​,…,ak​) 来表示其位置。场地有大小限制,第 ii 维的大小为 w_iwi​,因此处于场地中的人其坐标应满足 1 \le a_i \le w_i1≤ai​≤wi​(1 \le i \le k1≤i≤k)。

小 C 打算在接下来的 P = w_1 \times w_2 \times \cdots \times w_kP=w1​×w2​×⋯×wk​ 天中,每天从场地中一个新的位置出发,开始他的刷步数计划(换句话说,他将会从场地中每个位置都出发一次进行计划)。

他的计划非常简单,每天按照事先规定好的路线行进,每天的路线由 nn 步移动构成,每一步可以用 c_ici​ 与 d_idi​ 表示:若他当前位于 (a_1, a_2, \ldots , a_{c_i}, \ldots, a_k)(a1​,a2​,…,aci​​,…,ak​),则这一步他将会走到 (a_1, a_2, \ldots , a_{c_i} + d_i, \ldots , a_k)(a1​,a2​,…,aci​​+di​,…,ak​),其中 1 \le c_i \le k1≤ci​≤k,d_i \in \{-1, 1\}di​∈{−1,1}。小 C 将会不断重复这个路线,直到他走出了场地的范围才结束一天的计划。(即走完第 nn 步后,若小 C 还在场内,他将回到第 11 步从头再走一遍)。

小 C 对自己的速度非常有自信,所以他并不在意具体耗费的时间,他只想知道 PP 天之后,他一共刷出了多少步微信步数。请你帮他算一算。

输入格式

第一行两个用单个空格分隔的整数 n, kn,k。分别表示路线步数与场地维数。
接下来一行 kk 个用单个空格分隔的整数 w_iwi​,表示场地大小。
接下来 nn 行每行两个用单个空格分隔的整数 c_i, d_ici​,di​,依次表示每一步的方向,具体意义见题目描述。

输出格式

仅一行一个整数表示答案。答案可能很大,你只需要输出其对 {10}^9 + 7109+7 取模后的值。
若小 C 的计划会使得他在某一天在场地中永远走不出来,则输出一行一个整数 -1−1。

输入输出样例

输入 #1复制

3 2
3 3
1 1
2 -1
1 1

输出 #1复制

21

输入 #2复制

5 4
6 8 6 5
3 1
2 1
1 1
2 1
2 -1

输出 #2复制

10265

输入 #3复制

见附件中的 walk/walk3.in

输出 #3复制

见附件中的 walk/walk3.ans

输入 #4复制

见附件中的 walk/walk4.in

输出 #4复制

见附件中的 walk/walk4.ans

说明/提示

【样例 #1 解释】

从 (1, 1)(1,1) 出发将走 22 步,从 (1, 2)(1,2) 出发将走 44 步,从 (1, 3)(1,3) 出发将走 44 步。
从 (2, 1)(2,1) 出发将走 22 步,从 (2, 2)(2,2) 出发将走 33 步,从 (2, 3)(2,3) 出发将走 33 步。
从 (3, 1)(3,1) 出发将走 11 步,从 (3, 2)(3,2) 出发将走 11 步,从 (3, 3)(3,3) 出发将走 11 步。
共计 2121 步。

【数据范围】

测试点编号n \len≤k \lek≤w_i \lewi​≤
1 \sim 31∼3555533
4 \sim 64∼6100100331010
7 \sim 87∼8{10}^510511{10}^5105
9 \sim 129∼12{10}^510522{10}^6106
13 \sim 1613∼165 \times {10}^55×1051010{10}^6106
17 \sim 2017∼205 \times {10}^55×10533{10}^9109

对于所有测试点,保证 1 \le n \le 5 \times {10}^51≤n≤5×105,1 \le k \le 101≤k≤10,1 \le w_i \le {10}^91≤wi​≤109,d_i \in \{-1, 1\}di​∈{−1,1}。

称 nn 步为一轮,首先 -1−1 的情况很好判断:一轮后回到原地且在第一轮里存在某个起点走不出去。

我们把要求的答案转换一下:原本是考虑每个起点各自走多少步出界,现在转换成同时考虑所有起点,把每天还 存活的起点 数量计入贡献。(这里存活就是指从该起点出发到某天还没出界)

显然,只要把第 00 天活着的起点算进去(也就是 \prod w_i∏wi​),就和要算的答案等价了。

一共 mm 个维度,每个维度存活的位置是独立的,并且应是一段区间(只有开头、结尾的一部分会死亡)。

如果第 jj 维存活的区间是 [l_j,r_j][lj​,rj​],那总共存活的数量就为 \prod\limits_{j=1}^m(r_j-l_j+1)j=1∏m​(rj​−lj​+1) 。

根据这个想法,可以得到一个 O(nmT)O(nmT) 的算法,其中 TT 最长轮数,最坏情况下为 \max(w_j)max(wj​) 。

以下代码实测可以拿 4545分:

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;
}

下面考虑第 jj 维:

在第一轮第 ii 步时,历史移动最大位移为 [l_i,r_i][li​,ri​],那么死亡的起点数量应该为 r_i-l_iri​−li​ 个。

这是因为 [1,-l_i][1,−li​] 和 [n-r_i+1,n][n−ri​+1,n] 范围内的起点已经死了。

假设第一轮总偏移量为 e_jej​,在第二轮第 ii 步时,历史移动最大位移应为 [\min(l_i,e_j+l_i),\max(r_i,e_j+r_i)][min(li​,ej​+li​),max(ri​,ej​+ri​)]。

只要 e_j\neq 0ej​=0,无论 e_jej​ 的正负,会有起点在第二轮是新死的。

把第一轮结束时的最大位移 [l_n,r_n][ln​,rn​] 作为边界,求第二轮第 ii 步时的左右扩张范围 [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]);

那么 r'_i-l'_iri′​−li′​ 就是第二轮中 1\sim i1∼i 步里新死的人。

容易发现一个事实:第 2,3,4\cdots2,3,4⋯ 轮里每步的死亡情况是一致的,只有第一轮是特殊的。(可以画个图理解下,除了第一轮外,其他轮都存在被上一轮已经扩张过的地方,死过的起点不会再死一次)

有了这个周期规律就可以优化了:

首先第一轮单独算,只考虑第二轮开始的。

设第一轮后还活着 a_jaj​ 个起点,接下来每轮结束都有 b_jbj​ 个起点死亡,最后一轮的 1\sim i1∼i 步一共死了 f_i=r'_i-l'_ifi​=ri′​−li′​ 个点。

那么可以得到在 x+2x+2 轮的第 ii 步时,第 jj 维还活着 a_j-x\times b_j-f_iaj​−x×bj​−fi​ 个点,贡献为 \prod\limits_{j=1}^m a_j-x\times b_j-f_ij=1∏m​aj​−x×bj​−fi​。

设 T=\min\limits_{j=1}^m\frac{a_j-f_i}{b_j}T=j=1minm​bj​aj​−fi​​,那么我们需要外层枚举 ii ,内层枚举 x=0,1,2,\ldots,Tx=0,1,2,…,T 。

(注意这里可能出现 a_j-f_i\le 0aj​−fi​≤0 的情况,说明第二轮这个维度的起点就死光了,那后面就不用算了)

如果老老实实这样枚举 xx ,和之前做法就一样了。

要算的其实是 \sum\limits_{i=1}^n\sum\limits_{x=0}^T\prod\limits_{j=1}^m a_j-x\times b_j-f_ii=1∑n​x=0∑T​j=1∏m​aj​−x×bj​−fi​

内层 \prod∏ 展开后,得到一个关于 xx 的 mm 次多项式 G(x)G(x),这个多项式系数可以暴力 O(m^2)O(m2) 来算(我这里没有优化)

然后对多项式每项 p_ix^kpi​xk,只要单独算 \sum\limits_{x=0}^T x^kx=0∑T​xk 即可。

关于计算 \sum\limits_{i=1}^n i^ki=1∑n​ik 参考 传送门

而对本题而言, k\le 3k≤3 直接用公式,而 k > 3k>3 时,nn 不超过 10^6106 直接预处理也可以。

时间复杂度 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值