题意:m是长为n的数组,值均为非负数,前k个数按以下方式产生:
(1)m[0]=a;
(2)m[i] = (b * m[i - 1] + c) % r
其中1≤k≤100000,1≤n, a, b, c, r≤1000000000
对k ≤ j ≤ n - 1,m[j]是它前面k个数中没出现的最小非负数,形式地,m[j]=min{x|x ≥ 0 && x != m[j - 1] && x != m[j - 2] ... && x != m[j - k] }
求m[n - 1]
输入格式为:
T(测试案例数,每个测试案例2行)
n k
a b c r
n k
a b c r
.....
解:
首先要找规律,找个样例,用笨方法:把n个元素都按部就班算出来,打印输出看一下,比如:
97 39
34 37 656 97
首先把n改大一点,便于发现规律,打印输出如下(每k个元素1行)
34 71 82 4 28 43 16 84 78 50 81 64 17 24 89 69 8 79 87 92 83 41 39 62 40 2 51 21 75 36 48 7 42 76 73 59 26 66 91
0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34 35 36 37 26 38
39 0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34 35 36 37 26
38 39 0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34 35 36 37
26 38 39 0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34 35 36
37 26 38 39 0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34 35
36 37 26 38 39 0 1 3 5 4 6 9 10 11 12 13 14 15 16 17 18 19 8 20 22 23 24 25 27 28 29 2 30 21 31 32 33 7 34
可以看到,除去第一行,以k+1为周期,这意味着,只需要算出紧跟在前k个元素后面的k+1个元素就可以了
做题的时候,看到这一点就足够了,肯定没时间去证明,这里提供一个证明:
证明:m[j] = m[j - (k + 1)]对2k + 1 ≤ j ≤ n - 1成立
注意到:m[k - 1]之后的每个元素都在0~k这k+1个数之间,因为对任一个元素来讲,它不可以填前面k个数,所以0~k之间肯定有一个可以填
所以m[k],m[k+1]...m[2k]都在0~k之间,而其中任意两个元素不能相等,否则就违反题目要求,所以m[k],m[k+1]...m[2k]是0,1,...,k的一个排列
填m[2k+1]时,要求与m[k+1]...m[2k]不同,且最小,很显然,只能填m[k].更形式化一点:
(1)填m[k]肯定满足“与m[k+1]...m[2k]不同”
(2)填任一更小的非负数都会与m[k+1]...m[2k]中的某个相同
填完后,m[k+1]...m[2k],m[2k+1]也是0,1,...,k的一个排列,可以继续往下推m[2k+2]的值……
可以用数学归纳法完成这个证明。
O(k^3)的解法:填m[k...2k]共k+1个元素,对每个元素:从x=0开始检查x是否在它前面k个元素中出现,如果出现,x++,重新检查;如果不出现,填上x
注意到:填的每个元素都在0~k这k+1个数之间,因为对某个元素来讲,它不可以填前面k个数,所以0~k之间肯定有一个可以填
这样,我们可以维护一个数组,记录哪些数可以填,哪些不可以填,具体地:cnt[0..k],cnt[i]表示当前元素之前k个元素中i出现的次数
这样我们检查一个元素x是否在它前面k个元素中出现,就可以直接判断cnt[x]是否为0,这样复杂度就降到O(k^2),而1≤k≤100000,这就基本可用了
复杂度如何进一步降低呢?
考虑到我们每次都是从0开始检查,最坏情况要检查到k,有没有办法可以改进这一点呢?
可以维护一个的最小优先队列(最小堆),只包含0~k中没在当前元素m[j]前面k个元素中出现的数,即priority_queue = {x|k ≥ x ≥ 0 && x != m[j - 1] && x != m[j - 2] ... && x != m[j - k] }
这样每次取一个元素是O(lgk),整体复杂度为O(klgk)
这个问题复杂度可以降到O(k),这个方法来自justever86同学
为此,维护一个val值和与之相关的一个不变式:填当前元素时cnt[0],cnt[1],...cnt[val]均大于0
这样我们就不必每次都是从0开始检查,可以从val+1开始检查,填完后,更新val的值
具体地,
(1)填m[k]时,从0开始检查,找到最小的x,使得cnt[x] == 0,m[k]赋值为x,val赋值为x(不变式显然成立).然后对idx从k+1到2k循环执行(2)
(2)删去数组最前面的元素x
1.如果x在0~k之间,更新cnt数组的计数,即cnt[x]--
2.如果x <= val且cnt[x]==0,{m[idx]赋值为x,更新cnt[x],val保持不变}(解释:val前面连续的非零值中突然出现一个0,对应一个更小的可以填的数,将它填上,同时更新cnt,不变式仍成立);否则,从val + 1开始检查,找到最小的y,使得cnt[y] == 0,m[k]赋值为y,更新cnt[y],val赋值为y(不变式显然仍成立)
这个不变式保证了每次填入的数都是符合要求的数,考虑到val只增不减,val终值至多为k(填完m[2k],val应该恰好是k,终值应该恰为k),所以内循环平摊时间为常数(平摊到每个外循环),因此,总的复杂度为O(k)
//利用周期性,显然有周期性m[t] == m[t - (k + 1)] #include <stdio.h> #include <string.h> #include <vector> #include <iostream> #include <queue> #include <time.h> using namespace std; int T, n, k; long long a, b, c, r; int main(int argc, const char *argv[]) { vector<int> m; vector<int> cnt;//cnt记录当前最新的k个元素的出现次数 //freopen("in3.txt", "r", stdin); freopen("find_the_mintxt.txt", "r", stdin); //freopen("out3_1.txt", "w", stdout); cin >> T; for (int j = 1; j <= T; j++) { cin >> n >> k; cin >> a >> b >> c >> r; m.resize(2 * k + 1); cnt.resize(k + 1, 0);//resize真好用 m[0] = a; if (a <= k) cnt[a]++; for (int i = 1; i < k; i++) { m[i] = (b * m[i - 1] + c) % r; if (m[i] <= k) cnt[m[i]]++; } for (int i = 0; i <= k; i++) { if(cnt[i] == 0){ m[k] = i; cnt[i] = 1; break; } } int val = m[k]; //循环不变式为:cnt[0..val] >= 1 for (int ptr = 0; ptr < k; ptr++) { //delete m[ptr] if (m[ptr] <= k) cnt[m[ptr]]--; if (m[ptr] <= val && cnt[m[ptr]] == 0) { //下一个candidate是val + 1, 所以只要<=val都是更好的 m[ptr + k + 1] = m[ptr]; cnt[m[ptr]] = 1;//val不必更新 }else{ while (cnt[++val]); m[ptr + k + 1] = val; cnt[val] = 1; } } int idx = n - 1 - (n - k - 1) / (k + 1) * (k + 1); //printf("idx:%d\n", idx); printf("Case #%d: %d\n", j, m[idx]); m.clear(); cnt.clear(); } //printf("time used: %.2lf seconds\n", (double)clock() / CLOCKS_PER_SEC); return 0; }