2019牛客暑期多校训练营(第三场)
Contest Info
[Practice Link](https://ac.nowcoder.com/acm/contest/883#question)
Solved | A | B | C | D | E | F | G | H | I | J |
---|---|---|---|---|---|---|---|---|---|---|
8/10 | Ø | O | - | Ø | - | O | O | O | Ø | Ø |
- O 在比赛中通过
- Ø 赛后通过
- ! 尝试了但是失败了
- - 没有尝试
Solutions
A. Graph Games
题意:
有一张\(n\)个点\(m\)条边的图,定义\(S(x)\)为和\(x\)这个点邻接的所有点的集合。
支持两种操作:
- 翻转\([l, r]\)区间的边的状态,即已经在的话就删边,否则加边
- 询问\(S(u)\)是否等于\(S(v)\)
思路:
首先显示的表示\(S(x)\),对每个点随机一个权值,加点删点都是异或。
将点按度数进行分块,分为小点和大点。
再将边序列分块:
- 对于所有小点,块间打标记,块内暴力标记,查询的时候两个异或一下就知道当前边的状态
- 对于所有大点,维护出每块和它邻接的边的点的异或值,然后块间翻转,两侧暴力。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 200010
#define U 1010
#define unit 600
#define pii pair <int, int>
int n, m, q;
int vis[N], degree[N];
pii edge[N];
vector <int> vec[N];
int pos[N], posl[U], posr[U];
int isHasUnit[U], isHasSin[N];
int id[N];
int valBig[U][U], valBigOri[U][U];
int Hash[N], ran;
void read(int &res)
{
res = 0;
char c;
while (!isdigit(c = getchar()));
while (isdigit(c)) res = res * 10 + c - '0', c = getchar();
}
void force(int l, int r)
{
for (int i = l, u, v; i <= r; ++i)
{
isHasSin[i] ^= 1;
u = edge[i].first, v = edge[i].second;
if (vis[u])
valBig[id[u]][pos[i]] ^= Hash[v];
if (vis[v])
valBig[id[v]][pos[i]] ^= Hash[u];
}
}
void update(int l, int r)
{
if (pos[l] == pos[r]) force(l, r);
else
{
force(l, posr[pos[l]]);
for (int i = pos[l] + 1; i < pos[r]; ++i) isHasUnit[i] ^= 1;
force(posl[pos[r]], r);
}
}
int query(int u)
{
int res = 0;
if (vis[u])
{
for (int i = 1; i <= pos[m]; ++i)
{
res ^= valBig[id[u]][i];
if (isHasUnit[i]) res ^= valBigOri[id[u]][i];
}
}
else
{
for (auto it : vec[u])
{
if (isHasSin[it] ^ isHasUnit[pos[it]])
{
int v = edge[it].first == u ? edge[it].second : edge[it].first;
res ^= Hash[v];
}
}
}
return res;
}
void Run()
{
for (int i = 1; i <= 100000; ++i)
{
ran *= 233;
ran += 17;
Hash[i] = ran;
}
for (int i = 1; i <= 200000; ++i)
{
pos[i] = (i - 1) / unit + 1;
if (i == 1 || pos[i] != pos[i - 1]) posl[pos[i]] = i;
posr[pos[i]] = i;
}
int T; scanf("%d", &T);
while (T--)
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) degree[i] = 0, vis[i] = 0, vec[i].clear();
for (int i = 1; i <= m; ++i) isHasSin[i] = 1;
for (int i = 1; i <= pos[m]; ++i) isHasUnit[i] = 0;
id[0] = 0;
for (int i = 1, u, v; i <= m; ++i)
{
read(u), read(v);
edge[i] = pii(u, v);
++degree[u];
++degree[v];
}
for (int i = 1; i <= n; ++i) if (degree[i] >= unit)
{
vis[i] = 1;
id[i] = ++id[0];
for (int j = 1; j <= pos[m]; ++j)
valBig[id[i]][j] = 0, valBigOri[id[i]][j] = 0;
}
for (int i = 1, u, v; i <= m; ++i)
{
u = edge[i].first; v = edge[i].second;
if (vis[u])
{
valBig[id[u]][pos[i]] ^= Hash[v];
valBigOri[id[u]][pos[i]] ^= Hash[v];
}
else vec[u].push_back(i);
if (vis[v])
{
valBig[id[v]][pos[i]] ^= Hash[u];
valBigOri[id[v]][pos[i]] ^= Hash[u];
}
else vec[v].push_back(i);
}
read(q);
for (int qq = 1, op, x, y; qq <= q; ++qq)
{
read(op), read(x), read(y);
if (op == 1) update(x, y);
else putchar((query(x) == query(y)) + '0');
}
puts("");
}
}
int main()
{
Run();
return 0;
}
B. Crazy Binary String
题意:
有一个01串,要求选择一个最长的子串和子序列,使得其中\(0\)和\(1\)的个数相同。
思路:
- 子串:维护前缀和找一下相同值
- 子序列:Min(0的个数,1的个数)
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
int n;
char s[N];
int cnt[2], S[N];
map <int, int> mp;
int main() {
while (scanf("%d", &n) != EOF) {
scanf("%s", s + 1);
for (int i = 1; i <= n; ++i) S[i] = 0;
cnt[0] = cnt[1] = 0;
int res[2] = {0, 0};
for (int i = 1; i <= n; ++i) {
++cnt[s[i] - '0'];
if (s[i] == '1') {
--S[i];
} else {
++S[i];
}
S[i] += S[i - 1];
}
res[1] = min(cnt[0], cnt[1]) * 2;
mp.clear();
mp[0] = 0;
for (int i = 1; i <= n; ++i) {
if (mp.find(S[i]) != mp.end()) {
res[0] = max(res[0], i - mp[S[i]]);
} else {
mp[S[i]] = i;
}
}
printf("%d %d\n", res[0], res[1]);
}
return 0;
}
D. Big Integer
题意:
定义\(A(n) = 11111(n\text{个1})\),给出\(p, n, m\)询问有多少\((i, j)\)满足\(A(i^j) \equiv 0 \bmod p\)。
思路:
将\(A(n)\)写成:
那么有:
那么我们知道:
那么我们要找一个循环节\(t\),显然有\(t\;|\;9p\),那么直接暴力枚举\(9p\)的质因子,一个一个试着去掉就好了。
那么现在需要统计有多少对\((i, j)\)满足\(A(i^j) \equiv 0 \bmod p\)。
我们先令
那么对于一个\(j\),\(i\)需要满足\(g\;|\;i\),即:
并且考虑到\(q_i\)的最大取值只有\(30\),所以\(j > 30\)的情况和\(j = 30\)的情况是一样的,所以只需要枚举\(30\)次。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
#define ll long long
#define pii pair <int, int>
#define fi first
#define se second
ll p, n, m;
pii fac[N];
int tot;
struct node {
ll a[2][2];
node() {
memset(a, 0, sizeof a);
}
void set() {
a[0][0] = a[0][1] = 1;
}
node operator * (const node &other) const {
node res = node();
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
res.a[i][j] = (res.a[i][j] + a[i][k] * other.a[k][j] % p) % p;
}
}
}
return res;
}
}base, res;
ll eular(ll n) {
ll ans = n;
for (int i = 2; i * i <= n; ++i) {
if (n % i == 0) {
ans -= ans / i;
while (n % i == 0)
n /= i;
}
}
if (n > 1) ans -= ans / n;
return ans;
}
node qmod(node base, ll n) {
node res = node(); res.set();
while (n) {
if (n & 1) {
res = res * base;
}
base = base * base;
n >>= 1;
}
return res;
}
ll qpow(ll base, ll n) {
ll res = 1;
while (n) {
if (n & 1) {
res = res * base;
}
base = base * base;
n >>= 1;
}
return res;
}
void getfac(pii *fac, int &tot, ll x) {
tot = 0;
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
fac[++tot] = pii(i, 0);
while (x % i == 0) {
++fac[tot].se;
x /= i;
}
}
}
if (x != 1) fac[++tot] = pii(x, 1);
}
ll calc(ll t, ll n, ll m) {
ll res = 0;
getfac(fac, tot, t);
for (int j = 1; j <= min(30ll, m); ++j) {
ll g = 1;
for (int o = 1; o <= tot; ++o) {
int a = fac[o].fi, b = fac[o].se;
g *= qpow(a, b / j + (b % j != 0));
}
res += (n / g);
if (j == 30) res += (n / g) * (m - j);
}
return res;
}
int main() {
base = node();
base.a[0][0] = 10;
base.a[1][0] = 1;
base.a[1][1] = 1;
int T; scanf("%d", &T);
while (T--) {
scanf("%lld%lld%lld", &p, &n, &m);
if (p == 2 || p == 5) {
puts("0");
} else {
ll t = eular(p * 9);
getfac(fac, tot, t);
for (int i = 1; i <= tot; ++i) {
for (int j = 1; j <= fac[i].se; ++j) {
res = qmod(base, t / fac[i].fi - 1);
if (res.a[0][0] == 0) {
t = t / fac[i].fi;
} else {
break;
}
}
}
printf("%lld\n", calc(t, n, m));
}
}
return 0;
}
F. Planting Trees
题意:
在一个\(n \cdot m\)的矩形中,每个点有权值\(a_{i,j}\),现在要找到一个最大的矩形,使得矩形内部权值的最大值和最小值之差不超过\(M\)。
思路:
枚举上下界,单调队列维护最大最小值。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 510
#define INF 0x3f3f3f3f
#define pii pair <int, int>
#define fi first
#define se second
int a[N][N];
int g[2][N], que[2][N], l[2], r[2];
int main() {
int n, m;
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
scanf("%d", &a[i][j]);
}
}
int res = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
g[0][j] = -INF;
g[1][j] = INF;
}
for (int j = i; j <= n; ++j) {
l[0] = l[1] = 1;
r[0] = r[1] = 0;
int pos = 1;
for (int k = 1; k <= n; ++k) {
g[0][k] = max(g[0][k], a[j][k]);
g[1][k] = min(g[1][k], a[j][k]);
while (l[0] <= r[0] && g[0][k] >= g[0][que[0][r[0]]]) --r[0];
que[0][++r[0]] = k;
while (l[1] <= r[1] && g[1][k] <= g[1][que[1][r[1]]]) --r[1];
que[1][++r[1]] = k;
while (pos <= k && g[0][que[0][l[0]]] - g[1][que[1][l[1]]] > m) {
++pos;
for (int o = 0; o < 2; ++o) {
while (l[o] <= r[o] && que[o][l[o]] < pos) ++l[o];
}
}
if (pos <= k) {
res = max(res, (j - i + 1) * (k - pos + 1));
}
}
}
}
printf("%d\n", res);
}
return 0;
}
G. Removing Stones
题意:
先定义一种游戏:
有\(n\)堆石头,\(Bob\)每次可以选择两堆非空的各取走一个石头,如果最后能取光它就能获胜。
显然,这个游戏,如果\(n\)堆石头的总和是奇数,那么显然赢不了。
那么假如一条额外规则,如果总和是奇数,那么先去掉石头个数最小的那堆的一个石头。
现在询问有多少个区间\((l_i, r_i)\),使用这个区间内的石头进行游戏是必胜的。
思路:
首先注意到游戏必胜的条件是,区间的石头总数和大于等于两倍的区间最大值。
那么我们把区间的贡献都算在区间中拥有最大数量石头的那堆上,先处理出一个石头的管辖边界\([l, r]\),然后枚举一边,二分找另一边。
要枚举短的那边,这样复杂度是\(\mathcal{O}(nlog^2n)\)
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N 300010
#define pii pair <int, int>
#define fi first
#define se second
int n, a[N];
pii b[N];
ll S[N];
struct Cartesian_Tree {
struct node {
int id, val, fa;
int son[2];
node() {}
node (int id, int val, int fa) : id(id), val(val), fa(fa) {
son[0] = son[1] = 0;
}
bool operator < (const node &other) const {
return id < other.id;
}
}t[N];
int root;
void init() {
t[0] = node(0, 1e9, 0);
}
void build(int n, int *a) {
for (int i = 1; i <= n; ++i) {
t[i] = node(i, a[i], 0);
}
for (int i = 1; i <= n; ++i) {
int k = i - 1;
while (t[k].val < t[i].val) {
k = t[k].fa;
}
t[i].son[0] = t[k].son[1];
t[k].son[1] = i;
t[i].fa = k;
t[t[i].son[0]].fa = i;
}
root = t[0].son[1];
}
int DFS(int u) {
if (!u) return 0;
int lsze = DFS(t[u].son[0]);
int rsze = DFS(t[u].son[1]);
b[t[u].id].fi = lsze;
b[t[u].id].se = rsze;
return lsze + rsze + 1;
}
}CT;
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d", &n);
S[0] = 0;
for (int i = 1; i <= n; ++i) scanf("%d", a + i), S[i] = S[i - 1] + a[i];
CT.init();
CT.build(n, a);
CT.DFS(CT.root);
ll res = 0;
for (int i = 1; i <= n; ++i) {
int l = i - b[i].fi, r = i + b[i].se;
// cout << i << " " << l << " " << r << endl;
//枚举左边
if (i - l <= r - i) {
for (int j = l; j <= i; ++j) {
int ql = i, qr = r, pos = -1;
while (qr - ql >= 0) {
int mid = (ql + qr) >> 1;
if (S[mid] - S[j - 1] - a[i] >= a[i]) {
pos = mid;
qr = mid - 1;
} else {
ql = mid + 1;
}
}
if (pos != -1) {
res += r - pos + 1;
}
}
} else {
for (int j = i; j <= r; ++j) {
int ql = l, qr = i, pos = -1;
while (qr - ql >= 0) {
int mid = (ql + qr) >> 1;
if (S[j] - S[mid - 1] - a[i] >= a[i]) {
pos = mid;
ql = mid + 1;
} else {
qr = mid - 1;
}
}
if (pos != -1) {
res += pos - l + 1;
}
}
}
}
printf("%lld\n", res);
}
return 0;
}
H. Magic Line
题意:
二维平面上有\(n\)个点,问能否画一条直线使得直线上没有点,并且直线两边的点数相同。
思路:
如果\(x\)坐标没有重复,那么排序后直接从中间画一条直线。
否则微微倾斜一个角度。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 1010
struct node {
int x, y;
node() {}
node(int x, int y) : x(x), y(y) {}
void input() {
scanf("%d %d", &x, &y);
}
bool operator<(const node &other) const {
if (y == other.y) {
return x < other.x;
} else {
return y < other.y;
}
}
} a[N];
int n;
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
a[i].input();
}
sort(a + 1, a + 1 + n);
int mid = n >> 1;
if (a[mid].y == a[mid + 1].y) {
printf("%d %d %d %d\n", a[mid].x - 10000000, a[mid].y + 1, a[mid + 1].x + 10000000, a[mid].y - 1);
} else {
if (a[mid].x > a[mid + 1].x) {
printf("%d %d %d %d\n", -10000000, a[mid].y, 10000000, a[mid + 1].y);
} else {
printf("%d %d %d %d\n", -10000000, a[mid + 1].y, 10000000, a[mid].y);
}
}
}
return 0;
}
I. Median
题意:
有\(n\)个数\(a_i\),现在令\(b_i\)等于\(\{a_i, a_{i + 1}, a_{i + 2}\}\)三个数中的中位数,现在给出\(b_1 \cdots b_{n - 2}\),问能否还原\(a_i\)。
思路:
首先要考虑如果存在解,那么一定可以让\(a_i\)等于与它相关联的三个中位数。
因为如果不等于的话,那么\(a_i\)一定大于等于三个相关联的中位数,或者小于等于三个相关联的中位数,那么直接等于是没有问题的。
再考虑\(v[i][j](j \in [0, 2])\),第\(i\)个数中相关联的三个中位数的第\(j\)个(排序后的,其实不排序也是可以的)。
再考虑\(f[i][j][k] (j, k \in [0, 2])\),表示第\(i\)个数填\(v[i][j]\),第\(i - 1\)个数填\(v[i - 1][k]\),并且使得前\(i - 2\)个数都满足要求的情况下,当前方案是否能够成立。
然后枚举下一位转移即可,并且记录一下前驱,如果存在\(f[n][j][k]\)满足,那么直接找前驱即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100010
int n, a[N], b[N];
int f[N][3][3], pre[N][3][3], v[N][3], s[3];
int med(int x, int y, int z) {
s[0] = x, s[1] = y, s[2] = z;
sort(s, s + 3);
return s[1];
}
void get(int &x, int &y) {
x = -1, y = -1;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (f[n][i][j]) {
x = i;
y = j;
return;
}
}
void solve() {
int x, y; get(x, y);
if (x == -1 || y == -1) {
puts("-1");
return;
}
for (int i = n; i >= 1; --i) {
b[i] = v[i][x];
x = pre[i][x][y];
swap(x, y);
}
for (int i = 2; i < n; ++i) assert(med(b[i - 1], b[i], b[i + 1]) == a[i]);
for (int i = 1; i <= n; ++i) printf("%d%c", b[i], " \n"[i == n]);
}
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 2; i < n; ++i) scanf("%d", a + i);
a[0] = a[1] = a[2];
a[n] = a[n + 1] = a[n - 1];
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 3; ++j)
v[i][j] = a[i + j - 1];
sort(v[i], v[i] + 3);
}
for (int i = 1; i <= n; ++i)
for (int j = 0; j < 3; ++j)
for (int k = 0; k < 3; ++k)
f[i][j][k] = 0;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
f[2][i][j] = 1;
for (int i = 3; i <= n; ++i)
for (int j = 0; j < 3; ++j)
for (int k = 0; k < 3; ++k)
for (int l = 0; l < 3; ++l) {
if (!f[i - 1][k][l]) continue;
if (med(v[i - 2][l], v[i - 1][k], v[i][j]) != a[i - 1]) continue;
f[i][j][k] = 1;
pre[i][j][k] = l;
}
solve();
}
return 0;
}
J. LRU management
题意:
模拟\(LRU\)操作。
- 添加一个\(block\)到缓存中,如果已经存在,将它移到末尾
- 询问一个\(block\)是否在缓存中,如果存在,则输出它的前驱或者它本身或者它的后继的数据。
思路:
用\(map\)第一关键字存\(string\),第二关键字存链表的迭代器。\(mp.find\)好像要比\(mp.count\)要快?
这种题还是要仔细读题,想清楚再上手。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl "\n"
#define psi pair <ll, int>
#define fi first
#define se second
#define iter list<psi>::iterator
int q, m;
list <psi> lis;
unordered_map <ll, iter> mp;
ll change(char str[]){
ll r;
sscanf(str,"%lld",&r);
return r+10000000000ll*(strlen(str)-1);
}
void No() {
puts("Invalid");
}
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &q, &m);
mp.clear();
lis.clear();
int op, v; char ts[20]; ll s;
while (q--) {
scanf("%d%s%d", &op, ts, &v);
s = change(ts);
if (op == 0) {
if (mp.find(s) == mp.end()) {
lis.push_back(psi(s, v));
mp[s] = lis.end(); --mp[s];
printf("%d\n", v);
} else {
v = (*mp[s]).se;
printf("%d\n", v);
lis.erase(mp[s]);
lis.push_back(psi(s, v));
mp[s] = lis.end(); --mp[s];
}
if ((int)lis.size() > m) {
mp.erase(lis.front().fi);
lis.pop_front();
}
} else {
if (mp.find(s) == mp.end()) No();
else {
iter pos = mp[s];
if (v == 0) printf("%d\n", (*pos).se);
else if (v == -1) {
if (pos == lis.begin()) No();
else {
printf("%d\n", (*--pos).se);
}
} else {
if (pos == --lis.end()) No();
else {
printf("%d\n", (*++pos).se);
}
}
}
}
}
}
return 0;
}