UVA 12174 Shuffle
题意:有个音乐播放器有乱序功能,可以随机播放1 ~ s首歌曲,每次随机播放完后再次打乱顺序随机播放(在播放完这一组的歌曲之前,不会再次乱序,直到全部播完)。现截取一段记录,给定播放的歌曲数 s 以及这段记录的长度 n ,然后输入这段记录,根据这段记录,求随机排序发生的时刻有几种可能。(1 <= s, n <= 100000)
个人理解:我是真fw,先看了紫书的分析用滑动窗口,写了好几个小时,也没写出来,盯着lrj的代码看,看了半天最后才差不多理解了,利用了滑动窗口,最后将滑动窗口组合起来进行判断的方法很巧妙,这道题细节也很多,可以学到很多。
例子:4 10
3 4 4 1 3 2 1 2 3 4
1 空集合
2 3
3 3 4
4 3 4 4
5 3 4 4 1
6 4 4 1 3
7 4 1 3 2
8 …
…
11 1 2 3 4
12 2 3 4
13 3 4
14 4
15 空集合
集合总共10+4+1
1空集合+4正是从3 4 4 1之后开始划分,是有必要留着的,同理可得最后的15的空集合也要保留
特判例子(s>n):7 5
5 7 3 1 4
1 空集合
2 5
3 57
4 573
5 5731
6 57314
7 57314
8 57314
9 7314
10 314
11 14
12 4
13 空集合
联系下面的代码,剩下只有ok[s-1]值是0,其他都是1,所以得出特判的条件是n+1。怎么说,这个特判需要自己举例模拟领会,并不好理解。
下面写一下我对lrj代码的理解:
#include<iostream>
#include<vector>
using namespace std;
const int maxn = 100000 + 5;
int s, n, x[maxn * 3], cnt[maxn], ok[maxn * 2];
//cnt用来储存s个数字,每个数字出现的次数,x用来输入,OK用来判定该滑动窗口是否可行
int main() {
int T;
cin >> T;
while (T--) {
cin >> s >> n;
// 令x的初始状态为-1,补充2*s个数字,方便滑动窗口滑动(应该是这样,具体我还不太清楚,还需理解)
fill(x, x + n + 2 * s, -1);
for (int i = 0; i < n; i++) cin >> x[i + s];//从i+s的位置开始补充
int tot = 0; // 当前的滑动窗口有多少数字
fill(cnt + 1, cnt + s + 1, 0); //一共有s个数字,从cnt[1]-cnt[s]赋值0
fill(ok, ok + n + s + 1, 0); // 一共n+s+1个滑动窗口,ok用来检测第i个滑动窗口是否可行,ok[0]和ok[n+s]其实滑动窗口里面没有数字,但是需要,具体可以看上面的例子
//开始完成ok数组
for (int i = 0; i < n + s + 1; i++) {
if (tot == s) ok[i] = 1; //当前的滑动窗口个数刚好等于s,说明一个滑动窗口完成,ok=1
if (i < s && tot == i) ok[i] = 1; // 从左边开始滑动窗口中数字的个数逐渐增加,也算是一个滑动窗口,假如个数跟i是一样的,说明这个滑动窗口符合条件
if (i > n && tot == n + s - i) ok[i] = 1; // 从右边结束滑动窗口中数字的个数逐渐减少,也算是滑动窗口,同上
// 更新滑动窗口
if (i == n + s) break; //没有多余的滑动窗口了,可以直接退出
if (x[i] != -1 && --cnt[x[i]] == 0) tot--; // 滑动窗口的第一个数去掉
if (x[i + s] != -1 && cnt[x[i + s]]++ == 0) tot++; // 外面的下一个数进入滑动窗口
}
//检查所有可行的答案
int ans = 0;
for (int i = 0; i < s; i++) {//只需要从0到s-1就能判断所有的可能性
int valid = 1;
for (int j = i; j < n + s + 1; j += s)//j+的是s而不是1,因为每个滑动窗口之后再过s个数字才能接上上一个滑动窗口
if (!ok[j]) valid = 0;//valid=0,说明这种情况不行
if (valid) ans++;
}
if (ans == n + 1) ans = s; //特判
cout << ans << "\n";
}
return 0;
}