2019.9.21模拟赛
2019.9.21模拟赛
T1:奇怪的队列
类似蓝书《算法竞赛进阶指南》上树状数组一节中的一道例题。
不难想到先把所有人按身高排序,这样问题会变得容易下手。
身高按升序排序,然后依次为每个人在队列中安排位置。这样,假设我们已经安排到第x个人,他的身高和记住的数字分别是\(a_x\)、\(b_x\),现在队列中所有空余的位置上的人的身高都大于\(a_x\),已经安排好的x-1个位置上的人的身高都小于\(a_x\)。
问题转化为:找一个位置,满足该位置没有被占用,且该位置之前或之后恰有\(b_x\)个空位。一个位置有没有被占用可以用0/1表示,1表示空位(没有被占用),0表示已经被占用。这样就可以用前/后缀和表示,若第x个人的位置\(pos_x\)之前恰有\(b_x\)个空位,且\(pos_x\)本身也是空位,那么该位置前缀和\(sum_x=b_x+1\),同理可用后缀和表示\(pos_x\)之后恰有\(b_x\)个空位的情况。但同时维护前后缀和比较麻烦,于是我们尝试用前缀和表示\(pos_x\)之后恰有\(b_x\)个空位的情况:前x-1个人已经安排好位置,现在还有n-x+1个空位,\(pos_x\)之后有\(b_x\)个空位,那么该位置的前缀和就是\(sum_x=n-x+1-b_x\)。
01序列前缀和单调不减,于是我们可以二分找到满足要求的\(pos_x\)位置,分\(pos_x\)前/后有\(b_x\)个人两种情况,题目要求字典序最小,我们又是按升序排序,所以当两个位置都可以的时候选择靠前的位置即可。
注意我们每次安排好第x个人的位置,都会改变\(pos_x\)位置的0/1情况,即要求动态维护01序列的前缀和。我选择了用树状数组维护,每次需要二分\(pos_x\),对每次二分到的值在树状数组上查询前缀和,总复杂度\(O(nlog^2n)\)。
标程是用线段树维护01序列,利用线段树结构可直接在线段树上二分,类似平衡树查第k大元素的写法,这样复杂度为\(O(nlogn)\)。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 2e5 + 5, inf = 0x3f3f3f3f;
int n, q[N], c[N];
pii p[N];
void add(int p, int v) {
for(; p <= n; p += p & -p) c[p] += v;
}
int ask(int p) {
int ans = 0;
for(; p; p ^= p & -p) ans += c[p];
return ans;
}
int calc(int v) {
int l = 1, r = n, ans = n;
while(l <= r) {
int mid = (l + r) >> 1, res = ask(mid);
if(res >= v) ans = min(ans, mid), r = mid - 1;
else l = mid + 1;
}
return ask(ans) == v ? ans : inf;
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
scanf("%d%d", &p[i].first, &p[i].second);
add(i, 1);
}
sort(p + 1, p + n + 1);
for(int i = 1; i <= n; ++i) {
int pos = min(calc(p[i].second + 1), calc(n - i + 1 - p[i].second));
if(pos == inf) return puts("impossible"), 0;
q[pos] = p[i].first;
add(pos, -1);
}
for(int i = 1; i <= n; ++i) printf("%d ", q[i]);
return 0;
}
T2:质数
首先考虑单组询问的情况
注意到询问上界R=1e7,可以直接用bool数组标记符合要求的数。质数可以直接\(O(n)\)线性筛求出,对于两个质数的乘积,可以在求出的质数数组中两两枚举相乘进行标记。分析复杂度:看似两重循环,实际上内层循环在两数乘积大于R就break,所有枚举到的乘积都是合法的,且每两个质数的乘积只会被枚举一次。所以复杂度不会超过\(O(n)\)。
对于多组询问,对bool数组\(O(n)\)求出前缀和数组,每次\(O(1)\)回答即可。
Code:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 5;
int T, n, tot, l[N], r[N], p[N], sum[N];
bool mark[N], vis[N];
void prime() {
for(int i = 2; i <= n; ++i) {
if(!vis[i]) p[++tot] = i;
for(int j = 1; j <= tot && i * p[j] <= n; ++j) {
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
int main() {
scanf("%d", &T);
for(int i = 1; i <= T; ++i) {
scanf("%d%d", &l[i], &r[i]);
n = max(n, r[i]);
}
prime();
for(int i = 1; i <= tot; ++i) {
mark[p[i]] = true;
for(int j = i; j <= tot; ++j) {
if(1ll * p[i] * p[j] > n) break;
mark[p[i] * p[j]] = true;
}
}
for(int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + mark[i];
for(int i = 1; i <= T; ++i) printf("%d\n", sum[r[i]] - sum[l[i] - 1]);
return 0;
}
T3:好文章
字符串算法基本忘光了,只好打hash了。
思路很简单,对整个串hash,之后把每m个字符的hash值塞入set中,最后输出set的size,复杂度\(O(nlogn)\)。
数据较大,且可能经过构造,自然溢出会被卡。可以用双模数hash,选两个大质数和底数分别hash,最后比较pair类型。
最初写的时候函数传入参数是bool类型区分两个模数,结果传参时写成1和2,相当于单模数hash,WA了一整页...
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
const int N = 8e6 + 5;
const ll mod1 = 1e9 + 7, mod2 = 1e9 + 9, base1 = 131, base2 = 31;
int n, m;
ll hash1[N], hash2[N], fac_mod1[N], fac_mod2[N];
char s[N];
set<pll> U;
ll ask(int l, int r, bool op) {
if(op == 1) return ((hash1[r] - hash1[l - 1] * fac_mod1[r - l + 1] % mod1) % mod1 + mod1) % mod1;
else return ((hash2[r] - hash2[l - 1] * fac_mod2[r - l + 1] % mod2) % mod2 + mod2) % mod2;
}
int main() {
scanf("%d%d%s", &n, &m, s + 1);
fac_mod1[0] = fac_mod2[0] = 1;
for(int i = 1; i <= n; ++i) {
fac_mod1[i] = fac_mod1[i - 1] * base1 % mod1;
fac_mod2[i] = fac_mod2[i - 1] * base2 % mod2;
}
for(int i = 1; i <= n; ++i) {
hash1[i] = (hash1[i - 1] * base1 % mod1 + s[i] - 'a' + 1) % mod1;
hash2[i] = (hash2[i - 1] * base2 % mod2 + s[i] - 'a' + 1) % mod2;
}
for(int i = 1; i + m - 1 <= n; ++i)
U.insert(make_pair(ask(i, i + m - 1, 1), ask(i, i + m - 1, 0)));
cout << U.size();
return 0;
}