2019牛客暑期多校训练营(第八场)
A.All-one Matrices
枚举每一行作为极大矩阵的底部,然后枚举列根据(up[i][j])来确定矩阵高度,通过单调栈找到其左右最远扩展位置,之后通过预处理出行(1)个数的前缀和,判断一下下一行对应位置是否全为(1)即可。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3005;
int n, m;
int a[N][N], up[N][N];
char s[N][N];
int L[N], R[N];
int sta[N], sum[N][N];
set <pair<int, int> > S;
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> s[i] + 1;
for(int j = 1; j <= m; j++) {
a[i][j] = s[i][j] - '0';
sum[i][j] = (a[i][j] == 1 ? sum[i][j - 1] + 1 : 0);
}
}
ll ans = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
up[i][j] = (a[i][j] == 1 ? up[i - 1][j] + 1 : 0);
}
up[i][m + 1] = up[i][0] = -1;
int top = 0;
for(int j = 1; j <= m + 1; j++) {
if(top == 0) sta[++top] = j;
else {
while(top && up[i][sta[top]] > up[i][j]) {
R[sta[top]] = j; top--;
}
sta[++top] = j;
}
}
top = 0;
for(int j = m; j >= 0; j--) {
if(top == 0) sta[++top] = j;
else {
while(top && up[i][sta[top]] > up[i][j]) {
L[sta[top]] = j; top--;
}
sta[++top] = j;
}
}
S.clear();
for(int j = 1; j <= m; j++) {
int l = L[j] + 1, r = R[j] - 1;
if(l > r || a[i][j] == 0) continue;
if(sum[i + 1][r] - sum[i + 1][l - 1] != r - l + 1) {
S.insert(make_pair(l, r));
}
}
ans += (int)S.size();
}
cout << ans;
return 0;
}
/*
*/
B.Beauty Values
对于每一个右端点(r),在其之前的每个数的贡献即为其最后一次出现位置到起点的长度,维护一下即可。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int n;
int a[N], last[N];
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
ll ans = 0, sum = 0;
for(int i = 1; i <= n; i++) {
sum -= last[a[i]];
last[a[i]] = i;
sum += last[a[i]];
ans += sum;
}
cout << ans;
return 0;
}
C.CDMA
样例具有很强的暗示性,根据样例来构造答案即可。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2048;
int res[N][N];
int n;
void make(int x, int y, int z) {
if(z ==1) {
res[x][y] = 1;
return;
}
make(x, y, z / 2);
make(x + z / 2, y, z / 2);
make(x, y + z / 2, z / 2);
make(x + z / 2, y + z / 2, z / 2);
for(int i = x + z / 2; i <= x + z - 1; i++) {
for(int j = y + z / 2; j <= y + z - 1; j++) {
res[i][j] = -res[i][j];
}
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n;
make(1, 1, n);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
cout << res[i][j] << "
"[j == n];
return 0;
}
D.Distance
直接三维树状数组来搞:对于距离公式:(|x_1-x_2|+|y_1-y_2|+|z_1+z_2|),我们将其打开,只有下面两种情况(以(x)举例):
- (x_1-x_2,x_1leq x_2)
- (-x_1+x_2,x_1geq x_2)
因为树状数组维护的是前缀最大值,而第二种情况中限制为(x_1geq x_2),所以我们更新时就直接插入(max-x_1),对于(x_2)的询问,也直接询问(max-x_1)就行。
可以用一维数组模拟三维,代码如下:
Code
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int N = 3e5 + 5;
void gmax(int &x, int y) {
if(x < y) x = y;
}
void gmin(int &x, int y) {
if(x > y) x = y;
}
struct BIT{
int a[N];
int n, m, h;
int lowbit(int x) {return x & -x; }
int id(int i, int j, int k) {
return i * m * h + j * h + k;
}
void init(int _n, int _m, int _h) {
n = _n, m = _m, h = _h;
for(int i = 1; i < N; i++) a[i] = -INF;
}
void update(int x, int y, int z, int val) {
for(int i = x; i <= n; i += lowbit(i))
for(int j = y; j <= m; j += lowbit(j))
for(int k = z; k <= h; k += lowbit(k))
gmax(a[id(i, j, k)], val);
}
int query(int x, int y, int z) {
int ans = -INF;
for(int i = x; i; i -= lowbit(i))
for(int j = y; j; j -= lowbit(j))
for(int k = z; k; k -= lowbit(k))
gmax(ans, a[id(i, j, k)]);
return ans;
}
}bit[8];
int n, m, h, q;
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n >> m >> h >> q;
for(int i = 0; i < 8; i++) bit[i].init(n, m, h);
while(q--) {
int op, x, y, z;
cin >> op >> x >> y >> z;
if(op == 1) {
bit[0].update(x, y, z, x + y + z);
bit[1].update(x, y, h - z + 1, x + y - z);
bit[2].update(x, m - y + 1, z, x - y + z);
bit[3].update(x, m - y + 1, h - z + 1, x - y - z);
bit[4].update(n - x + 1, y, z, -x + y + z);
bit[5].update(n - x + 1, m - y + 1, z, -x - y + z);
bit[6].update(n - x + 1, y, h - z + 1, -x + y - z);
bit[7].update(n - x + 1, m - y + 1, h - z + 1, -x - y - z);
} else {
int ans = INF;
gmin(ans, x + y + z - bit[0].query(x, y, z));
gmin(ans, x + y - z - bit[1].query(x, y, h - z + 1));
gmin(ans, x - y + z - bit[2].query(x, m - y + 1, z));
gmin(ans, x - y - z - bit[3].query(x, m - y + 1, h - z + 1));
gmin(ans, -x + y + z - bit[4].query(n - x + 1, y, z));
gmin(ans, -x - y + z - bit[5].query(n - x + 1, m - y + 1, z));
gmin(ans, -x + y - z - bit[6].query(n - x + 1, y, h - z + 1));
gmin(ans, -x - y - z - bit[7].query(n - x + 1, m - y + 1, h - z + 1));
cout << ans << '
';
}
}
return 0;
}
还有一种解法就是定期重构。
设定一个阀值(E),对于插入点的操作,我们先把所有点装入一个桶中,当点的数量超过了这个阀值,就将点全取出来跑次(bfs)找最短路。
对于询问就在(dis)和桶中点取(min)即可。
当(E)取(sqrt{nmq})时总时间复杂度最小。
Code
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int N = 3e5 + 5, E = 666;
int n, m, h, q;
int id(int x, int y, int z) {
return x * m * h + y * h + z;
}
struct node{
int x, y, z;
};
vector <node> c;
int dis[N];
int dx[6] = {-1, 1, 0, 0, 0, 0};
int dy[6] = {0, 0, 1, -1, 0, 0};
int dz[6] = {0, 0, 0, 0, 1, -1};
bool in(int x, int y, int z) {
return x <= n && x && y <= m && y && z <= h && z;
}
void rebuild() {
queue <node> q;
for(auto it : c) {
q.push({it.x, it.y, it.z});
dis[id(it.x, it.y, it.z)] = 0;
}
while(!q.empty()) {
node u = q.front(); q.pop();
for(int i = 0; i < 6; i++) {
int curx = u.x + dx[i];
int cury = u.y + dy[i];
int curz = u.z + dz[i];
if(in(curx, cury, curz) && dis[id(curx, cury, curz)] > dis[id(u.x, u.y, u.z)] + 1) {
dis[id(curx, cury, curz)] = dis[id(u.x, u.y, u.z)] + 1;
q.push({curx, cury, curz});
}
}
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
memset(dis, INF, sizeof(dis));
cin >> n >> m >> h >> q;
while(q--) {
int op, x, y, z;
cin >> op >> x >> y >> z;
if(op == 1) {
c.push_back({x, y, z});
if(c.size() >= E) {
rebuild();
c.clear();
}
} else {
int ans = dis[id(x, y, z)];
for(auto it : c) {
ans = min(ans, abs(x - it.x) + abs(y - it.y) + abs(z - it.z));
}
cout << ans << '
';
}
}
return 0;
}
E.Explorer
考虑最暴力的做法,枚举(size)然后拿出所有能通过的边来判断连通性。
但题目的数据范围很大,显然这样不行。
观察发现如果把所有的能通行区间([l,r])拿出来,放在一维线段上,每一个小区间都具有相同的性质,也就是说它们要么都行,要么都不行。
所以就考虑线段树维护区间信息。因为我们还需要判断连通性,对于每个线段树中的结点,储存一些边,意即结点子树中所有点都可以具有这些边,然后跑一遍(dfs),往下的时候合并判连通性,回溯时撤销相关操作即可。
因为支持撤销操作,所以就不能路径压缩,就采用启发式合并。
复杂度(O(nlog^2n))。
Code
#include <bits/stdc++.h>
#define MP make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 2e5 + 5;
int n, m;
struct node{
int u, v, l, r;
}a[N];
int b[N], D;
vector <int> c[N << 2];
vector <pair<int, int> > d[100];
ll ans;
void insert(int o, int l, int r, int L, int R, int id) {
if(L <= l && r <= R) {
c[o].push_back(id);
return;
}
int mid = (l + r) >> 1;
if(L <= mid) insert(o << 1, l, mid, L, R, id);
if(R > mid) insert(o << 1|1, mid + 1, r, L, R, id);
}
int f[N], sz[N];
int find(int x) {
return f[x] == x ? x : find(f[x]);
}
void merge(int x, int y, int dep) {
int fx = find(x), fy = find(y);
if(fx == fy) return;
if(sz[fx] > sz[fy]) swap(fx, fy);
int tmp = 0;
f[fx] = fy;
if(sz[fx] == sz[fy]) tmp++;
sz[fy] += tmp;
d[dep].push_back(MP(fx, tmp));
}
void del(int dep) {
for(auto it : d[dep]) {
sz[f[it.first]] -= it.second;
f[it.first] = it.first;
}
d[dep].clear();
}
void dfs(int o, int l, int r, int dep) {
for(auto it : c[o]) {
merge(a[it].u, a[it].v, dep);
}
if(find(1) == find(n)) {
ans += b[r + 1] - b[l];
} else if(l < r) {
int mid = (l + r) >> 1;
dfs(o << 1, l, mid, dep + 1);
dfs(o << 1|1, mid + 1, r, dep + 1);
}
del(dep);
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
cin >> a[i].u >> a[i].v >> a[i].l >> a[i].r;
b[++D] = a[i].l, b[++D] = a[i].r + 1;
}
sort(b + 1, b + D + 1);
D = unique(b + 1, b + D + 1) - b - 1;
for(int i = 1; i <= m; i++) {
a[i].l = lower_bound(b + 1, b + D + 1, a[i].l) - b;
a[i].r = lower_bound(b + 1, b + D + 1, a[i].r + 1) - b - 1;
insert(1, 1, D, a[i].l, a[i].r, i);
}
for(int i = 1; i <= n; i++) f[i] = i, sz[i] = 1;
dfs(1, 1, D, 1);
cout << ans;
return 0;
}
G.Gemstones
签到题,用数组模拟栈即可。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int n, m;
char s[N];
int sta[N];
bool check(int p) {
if(p < 3) return false;
if(sta[p] == sta[p - 1] && sta[p - 1] == sta[p - 2]) return true;
return false;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> s + 1;
n = strlen(s + 1);
int top = 0, ans = 0;
for(int i = 1; i <= n; i++) {
if(top == 0) {
sta[++top] = s[i] - 'A';
} else {
sta[++top] = s[i] - 'A';
if(check(top)) {
top -= 3;
ans++;
}
}
}
cout << ans;
return 0;
}
I.Inner World
因为题目中给出的所有(u,v)都不相同,那么每个结点的父亲都是确定的,只是存在区间不同。
所以考虑直接将图建在一棵树上,每个结点有个区间信息,表示这个结点出现在哪些区间中。
现在要求的其实就是子树中所有区间与([l,r])的交集。
这里有两种做法,但都是将问题转化为了"区间的区间和"。
考虑求出(dfs)序,那么子树中的(dfs)序是连续的,维护子树中的信息就可以用主席树来搞,然后主席树上面维护区间和的信息就行了。
感觉这种写法还是挺有意思的,(insert)操作时因为要求数据更新的正确性,所以对应区间范围会跟着变化;然后维护一个懒标记,懒标记下面的结点就不用建出来了,反正全都要加上(1)。查询时记得加上懒标记的贡献。
Code
#include <bits/stdc++.h>
#define fi first
#define se second
#define MP make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1000005;
int n, m, q;
pii a[N];
vector <int> g[N];
int in[N], out[N], mp[N], T;
void dfs(int u) {
in[u] = ++T;
mp[T] = u;
for(auto v : g[u]) dfs(v);
out[u] = T;
}
int rt[N], ls[N * 20], rs[N * 20], lz[N * 20];
ll sumv[N * 20];
int tot;
void build(int &o, int l, int r) {
o = ++tot;
if(l == r) return;
int mid = (l + r) >> 1;
build(ls[o], l, mid);
build(rs[o], mid + 1, r);
}
void insert(int &o, int last, int l, int r, int L, int R) {
lz[o = ++tot] = lz[last];
ls[o] = ls[last]; rs[o] = rs[last];
sumv[o] = sumv[last] + R - L + 1;
if(L <= l && r <= R) {
lz[o]++;
return;
}
int mid = (l + r) >> 1;
if(L <= mid) insert(ls[o], ls[last], l, mid, L, min(R, mid));
if(R > mid) insert(rs[o], rs[last], mid + 1, r, max(mid + 1, L), R);
}
ll query(int &o, int last, int l, int r, int L, int R, ll add) {
if(L <= l && r <= R) return sumv[o] - sumv[last] + (r - l + 1) * add;
add += lz[o] - lz[last];
int mid = (l + r) >> 1;
ll res = 0;
if(L <= mid) res += query(ls[o], ls[last], l, mid, L, R, add);
if(R > mid) res += query(rs[o], rs[last], mid + 1, r, L, R, add);
return res;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v, l, r;
cin >> u >> v >> l >> r;
a[v] = MP(l, r);
g[u].push_back(v);
}
a[1] = MP(1, n);
dfs(1);
build(rt[0], 1, n);
for(int i = 1; i <= T; i++) {
int now = mp[i];
insert(rt[i], rt[i - 1], 1, n, a[now].fi, a[now].se);
}
cin >> q;
while(q--) {
int x, l, r; cin >> x >> l >> r;
ll ans = query(rt[out[x]], rt[in[x] - 1], 1, n, l, r, 0);
cout << ans << '
';
}
return 0;
}
另外还可以直接维护子树增量,进入子树前答案先减去目前([l,r])的和,从子树出来后再把答案加上目前([l,r])的和,这样就可以维护子树中区间([l,r])的增量了。
Code
#include <bits/stdc++.h>
#define fi first
#define se second
#define MP make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 300005;
int n, m, q;
pii a[N];
vector <int> g[N];
struct Query{
int op, l, r, id;
};
int in[N], out[N], mp[N], T;
ll res[N];
vector <Query> vec[N];
void dfs(int u) {
in[u] = ++T;
mp[T] = u;
for(auto v : g[u]) dfs(v);
out[u] = T;
}
ll sumv[N << 2];
int lz[N << 2];
void push_down(int o, int l, int r) {
if(lz[o]) {
int mid = (l + r) >> 1;
lz[o << 1] += lz[o];
lz[o << 1|1] += lz[o];
sumv[o << 1] += 1ll * lz[o] * (mid - l + 1);
sumv[o << 1|1] += 1ll * lz[o] * (r - mid);
lz[o] = 0;
}
}
void push_up(int o) {
sumv[o] = sumv[o << 1] + sumv[o << 1|1];
}
void update(int o, int l, int r, int L, int R) {
if(L <= l && r <= R) {
sumv[o] += r - l + 1;
lz[o]++; return;
}
push_down(o, l, r);
int mid = (l + r) >> 1;
if(L <= mid) update(o << 1, l, mid, L, R);
if(R > mid) update(o << 1|1, mid + 1, r, L, R);
push_up(o);
}
ll query(int o, int l, int r, int L, int R) {
if(L <= l && r <= R) return sumv[o];
push_down(o, l, r);
int mid = (l + r) >> 1;
ll res = 0;
if(L <= mid) res += query(o << 1, l, mid, L, R);
if(R > mid) res += query(o << 1|1, mid + 1, r, L, R);
return res;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v, l, r;
cin >> u >> v >> l >> r;
a[v] = MP(l, r);
g[u].push_back(v);
}
a[1] = MP(1, n);
dfs(1);
cin >> q;
for(int i = 1; i <= q; i++) {
int x, l, r; cin >> x >> l >> r;
vec[in[x] - 1].push_back(Query{-1, l, r, i});
vec[out[x]].push_back(Query{1, l, r, i});
}
for(int i = 1; i <= T; i++) {
update(1, 1, n, a[mp[i]].fi, a[mp[i]].se);
for(auto it : vec[i]) {
res[it.id] += it.op * query(1, 1, n, it.l, it.r);
}
}
for(int i = 1; i <= q; i++) cout << res[i] << '
';
return 0;
}
J.Just Jump
首先求出(dp[i]),表示跳跃了(i)的距离共有多少种方法。
然后考虑减去某些不合法的情况。
这里可以容斥来搞,但还有一种更加简单的办法,就是对于每个(p_i),单独求出其对答案的贡献,然后减去即可。
详见代码吧:
Code
#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std;
typedef pair<int, int> pii;
typedef long long ll;
const int N = 1e7 + 5, M = 3005, MOD = 998244353;
int l, d, m;
int add(int x, int y) {
x = (x + y) % MOD;
if(x < 0) x += MOD;
return x;
}
int mul(ll x, int y) {
x = x * y % MOD;
if(x < 0) x += MOD;
return x;
}
int dp[N], sum[N];
int fac[N], inv[N];
int qp(int a, int b) {
int ans = 1;
while(b) {
if(b & 1) ans = mul(ans, a);
a = mul(a, a);
b >>= 1;
}
return ans;
}
void pre() {
dp[0] = sum[0] = 1;
for(int i = 1; i <= l; i++) {
if(i - d >= 0) dp[i] = sum[i - d];
sum[i] = add(sum[i - 1], dp[i]);
}
fac[0] = 1;
for(int i = 1; i <= l; i++) fac[i] = mul(fac[i - 1], i);
inv[l] = qp(fac[l], MOD - 2);
for(int i = l - 1; i >= 0; i--) inv[i] = mul(inv[i + 1], i + 1);
}
pii a[M];
int f[M];
int C(ll n, int m) {
if(n < 0 || m < 0 || n < m) return 0;
return 1ll * fac[n] * inv[m] % MOD * inv[n - m] % MOD;
}
int h(int l, int r, int k) {
return C(r - l - 1ll * (d - 1) * k - 1, k - 1);
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
cin >> l >> d >> m;
pre();
for(int i = 1; i <= m; i++) cin >> a[i].fi >> a[i].se;
sort(a + 1, a + m + 1);
m = unique(a + 1, a + m + 1) - a - 1;
int ans = dp[l];
for(int i = 1; i <= m; i++) {
f[i] = h(0, a[i].se, a[i].fi);
for(int j = 1; j < i; j++) f[i] = add(f[i], -mul(f[j], h(a[j].se, a[i].se, a[i].fi - a[j].fi)));
ans = add(ans, -mul(f[i], dp[l - a[i].se]));
}
cout << ans;
return 0;
}