链接:
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=3326
题意:
你正在使用的音乐播放器有一个所谓的乱序功能,即随机打乱歌曲的播放顺序。
假设一共有s首歌,则一开始会给这s首歌随机排序,全部播放完毕后再重新随机排序、继续播放,依此类推。
注意,当s首歌播放完毕之前不会重新排序。这样,播放记录里的每s首歌都是1~s的一个排列。
给出一个长度为n(1≤s,n≤100000)的播放记录(不一定是从最开始记录的),你的任务是统计下次随机排序所发生的时间有多少种可能性。
例如,s=4,播放记录是3, 4, 4, 1, 3, 2, 1, 2, 3, 4,不难发现只有一种可能性:前两首是一个段的最后两首歌,后面是两个完整的段,因此答案是1;
当s=3时,播放记录1, 2, 1有两种可能:第一首是一个段,后两首是另一段;前两首是一段,最后一首是另一段。答案为2。
分析1:
使用滑动窗口,窗口大小是“基本”固定的(因为还需要考虑不完整的段),因此只需要一个指针;
而且所有数都是1~s的整数,只需要一个数组即可保存每个数在窗口中出现的次数。
再用一个变量记录在窗口中恰好出现一次的数的个数,则可以在O(n)时间内判断出每个窗口是否满足要求(每个整数最多出现一次)。
这样,就可以枚举所有可能的答案,判断它对应的所有窗口,当且仅当所有窗口均满足要求时这个答案是可行的。
可以把所有窗口都映射成窗口0 ~ s-1,使用求模即可。
代码1:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 using namespace std; 5 6 const int UP = 100000 + 5; 7 8 int a[UP], num[UP]; //num[x]记录当前窗口中x值的个数 9 bool can[UP]; //can[i]表示以位置i结尾的一个序列是否可能是1~s的一个排列 10 11 int main(){ 12 int T; 13 scanf("%d", &T); 14 while(T--){ 15 int s, n; 16 scanf("%d%d", &s, &n); 17 for(int i = 0; i < n; i++) scanf("%d", &a[i]); 18 19 memset(num, 0, sizeof(num)); 20 memset(can, true, sizeof(can)); 21 22 int tot = 0; //tot记录当前窗口中只出现一次的数的个数 23 for(int i = 0; i < n + s; i++){ 24 if(i < n && ++num[a[i]] == 1) tot++; 25 if(i >= s && --num[a[i-s]] == 0) tot--; 26 if(min(i, n-1) - max(i-s, -1) != tot) can[i%s] = false; 27 } 28 29 int ans = 0; 30 for(int i = 0; i < s; i++) if(can[i]) ans++; 31 printf("%d ", ans); 32 } 33 return 0; 34 }
分析2:
这个比较直观。对于1 2 1这样的播放列表,两个1之间必然存在一个窗口的交界位置。类似地,对于同一个数字的两次相邻的出现,
都能排除一些不可能的位置,当把所有不可能的位置排除后,剩下的位置个数便是答案。
可以把所有位置都映射成位置0 ~ s-1,使用求模即可。
代码2:
1 #include <cstdio> 2 #include <cstring> 3 4 const int UP = 100000 + 5; 5 6 int a[UP], f[UP]; //f[a[i]] 记录与 a[i] 的值相同的前一个位置 7 bool can[UP]; //can[i]表示以位置i结尾的一个序列是否可能是1~s的一个排列 8 9 int main(){ 10 int T; 11 scanf("%d", &T); 12 while(T--){ 13 int s, n; 14 scanf("%d%d", &s, &n); 15 for(int i = 0; i < n; i++) scanf("%d", &a[i]); 16 17 memset(f, -1, sizeof(f)); 18 memset(can, true, sizeof(can)); 19 20 for(int x, i = 0; i < n; f[x] = i++){ 21 x = a[i]; 22 if(f[x] != -1){ //判断x之前是否出现过 23 if(i - f[x] >= s) continue; //相邻的两个x的距离不小于s,则位置f[x]~i-1都可能是结尾 24 int L = f[x] % s, R = i % s; //把位置L和R都映射成位置0 ~ s-1 25 if(L < R){ 26 for(int t = 0; t < L; t++) can[t] = false; 27 for(int t = R; t < s; t++) can[t] = false; 28 } 29 else for(int t = R; t < L; t++) can[t] = false; 30 } 31 } 32 33 int ans = 0; 34 for(int i = 0; i < s; i++) if(can[i]) ans++; 35 printf("%d ", ans); 36 } 37 return 0; 38 }
分析3:
这是我自己想到的做法,跟分析2基本相同,只是没有使用求模来映射位置,速度要慢一点。在这里也把代码贴一下吧。
代码3:
1 #include <cstdio> 2 #include <cstring> 3 4 const int UP = 100000 + 5; 5 6 int a[UP], f[UP]; //f[a[i]] 记录与 a[i] 的值相同的前一个位置 7 bool no[UP*2]; //no[i]表示以位置i结尾的一个序列是否可能是1~s的一个排列 8 9 int main(){ 10 int T; 11 scanf("%d", &T); 12 while(T--){ 13 int s, n; 14 scanf("%d%d", &s, &n); 15 for(int i = 1; i <= n; i++) scanf("%d", &a[i]); 16 17 memset(f, 0, sizeof(f)); 18 memset(no, false, sizeof(no)); 19 20 for(int x, i = 1; i <= n; f[x] = i++){ 21 x = a[i]; 22 if(f[x]){ //判断x之前是否出现过 23 if(i - f[x] >= s) continue; //相邻的两个x的距离不小于s,则位置f[x]~i-1都可能是结尾 24 for(int t = i; t - f[x] < s; t++) no[t] = true; //排除不可能是结尾的位置 25 for(int t = f[x] - 1; t && i - t <= s; t--) no[t] = true; 26 } 27 } 28 29 int ans = 0; 30 for(int t, i = 1; i <= s; i++){ 31 for(t = i; t <= n + s; t += s) if(no[t]) break; //判断以位置i为结尾是否可行 32 if(t > n + s) ans++; 33 } 34 printf("%d ", ans); 35 } 36 return 0; 37 }