补题写题解,即可得牛币https://ac.nowcoder.com/discuss/988926
A
首先简化一下题意,等价于有 个区间
,定义两个区间连通当且仅当它们交集非空,你可以任意添加区间,求使得
个区间连通需要添加的最小区间长度和。
将所有区间按 排序,然后每个合并小区间得到的大区间可以表示为下标的一个连续段,填补大区间之间的空隙即可。
时间复杂度 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 9;
int ans, id[N], n, r[N], x[N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
id[i] = i, scanf("%d%d", &x[i], &r[i]);
sort(id + 1, id + n + 1, [&](int a, int b) { return x[a] - r[a] < x[b] - r[b]; });
for (int ii = 2, mx = x[id[1]] + r[id[1]]; ii <= n; ++ii) {
int i = id[ii];
if (x[i] - r[i] > mx)
ans += x[i] - r[i] - mx;
mx = max(mx, x[i] + r[i]);
}
printf("%d", ans);
return 0;
}
C
首先对于 和
的行特判一下,他们会挡住后面的所有人。
一个点造成的影响就是 两个点到他的射线的交。
对于同一行的两个点 ,其中
,第一个点的影响范围会完全包含第二个点,所以有用的点只有
个。
画图会发现,造成的影响是一个折线图。具体的,对于一行 ,会存在一个
表示
都合法,
都不合法。并且我们可以做两遍,第一次求出
对每行的影响,再求出
对每行的影响,每一行的
最终就是两次求得的较小值。
到此有个显然的做法就是直接李超树,但是时间复杂度无法接受,我们需要一个线性的做法。
以 到每个点为例。从小到大枚举每一行
,然后比较已有射线和当前射线
(其中
表示第
行横坐标最小的点。),更新为斜率较大的一条,然后更新这一行的
。
时间复杂度 。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 9;
int n, m, k, q;
int x1[N], mn[N];
int X[N], Y[N];
signed main() {
cin >> n >> m >> k >> q;
for (int i = 1; i <= k; i++)
scanf("%lld%lld", &X[i], &Y[i]);
while (q--) {
int id;
cin >> id;
scanf("%lld%lld", &X[id], &Y[id]);
for (int i = 1; i <= m; i++)
x1[i] = n + 1, mn[i] = n;
for (int i = 1; i <= k; i++)
x1[Y[i]] = min(x1[Y[i]], X[i]);
int j = 0;
for (int i = 1; i <= m; i++) { //(0,1)
// (0,1,x1[i],i) (0,1,x1[j],j)
if (x1[i] != n + 1 && (!j || x1[i] * (j - 1) - (i - 1)*x1[j] < 0))
j = i;
if (j == 1)
mn[i] = min(mn[i], i == 1 ? x1[i] - 1 : n);
else
mn[i] = min(mn[i], j ? ((i - 1) * x1[j] - 1) / (j - 1) : n);
}
j = 0;
for (int i = m; i >= 1; i--) { //(0,m)
// (0,m,x1[i],i) (0,m,x1[j],j)
if (x1[i] != n + 1 && (!j || x1[i] * (j - m) - (i - m)*x1[j] > 0))
j = i;
if (j == m)
mn[i] = min(mn[i], i == m ? x1[i] - 1 : n);
else
mn[i] = min(mn[i], j ? ((m - i) * x1[j] - 1) / (m - j) : n);
}
int ans = 0;
for (int i = 1; i <= m; i++)
ans += mn[i];
cout << ans << endl;
}
return 0;
}
D
首先根据圆的对称性, 和
是等效的,于是只需考虑
的答案。
显然摧毁的墙壁是一段劣弧,那么长度的大小关系就可以映射到弦长的大小关系。设电磁炮摧毁墙壁的端点为 ,则对应弦长长度为
,
为定值,那么显然两点纵坐标之差的绝对值越大时弦长越大。
设 的两端点分别为
,不妨设
,令
为
的
值,则有
,显然此时
垂直于
轴,即
,弧长的计算使用极角相关知识即可。
#include <bits/stdc++.h>
using namespace std;
typedef long double ll;
int main() {
int t_case;
scanf("%d", &t_case);
while (t_case--) {
ll d, r, x, y;
scanf("%Lf%Lf%Lf%Lf", &r, &x, &y, &d);
ll p = sqrt(x * x + y * y);
ll L = p - d, R = p + d;
printf("%.10Lf\n", (acos(L / r) - acos(R / r)) * r);
}
return 0;
}
E
设 表示第一棵树上以
为根的子树和第二棵树上以
为根的子树的最大公共子序列的大小。
考虑更新 ,首先可以从所有的
取最大值继承过来。
若 ,那么
可以匹配。只需要再将
的儿子和
的儿子两两匹配,得到最大权值。
每次更新都枚举 ,然后建立一条边
,边权为
。跑一遍最大权匹配,用最后所得的答案
去更新
即可。
分析时间复杂度。假设左部点有 个,右部点有
个,且
,增广的时间复杂度就是
那么时间复杂度就是 级别。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 9;
const int INF = 1e9;
int c1[N], c2[N];
vector<int>e1[N], e2[N];
int dp[N][N], w[N][N];
int km(int n, int m) {
vector<int>u(n + 1), v(m + 1), p(m + 1), Fa(m + 1);
for (int i = 1; i <= n; i++) {
p[0] = i;
int a = 0;
vector<int>mn(m + 1, INF);
vector<bool>used(m + 1, false);
do {
used[a] = true;
int i0 = p[a], del = INF, b = 0;
for (int j = 1; j <= m; ++j)
if (!used[j]) {
int val = w[i0][j] - u[i0] - v[j];
if (val < mn[j])
mn[j] = val, Fa[j] = a;
if (mn[j] < del)
del = mn[j], b = j;
}
for (int j = 0; j <= m; ++j)
if (used[j])
u[p[j]] += del, v[j] -= del;
else
mn[j] -= del;
a = b;
} while (p[a] != 0);
do {
int b = Fa[a];
p[a] = p[b];
a = b;
} while (a);
}
return -v[0];
}
int n, m;
void dfs2(int x, int fx, int i, int fa) {
for (auto to : e2[i])
if (to != fa)
dfs2(x, fx, to, i);
for (auto to : e2[i])
if (to != fa)
dp[x][i] = max(dp[x][i], dp[x][to]);
for (auto to : e1[x])
if (to != fx)
dp[x][i] = max(dp[x][i], dp[to][i]);
if (c1[x] == c2[i]) {
int f = (e1[x].size() <= e2[i].size());
for (int j = 0; j < e1[x].size(); j++)
for (int k = 0; k < e2[i].size(); k++) {
int v = -dp[e1[x][j]][e2[i][k]] * (e1[x][j] != fx && e2[i][k] != fa);
f ? (w[j + 1][k + 1] = v) : (w[k + 1][j + 1] = v);
}
if (f)
dp[x][i] = max(dp[x][i], 1 - km(e1[x].size(), e2[i].size()));
else
dp[x][i] = max(dp[x][i], 1 - km(e2[i].size(), e1[x].size()));
}
}
void dfs(int x, int fa) {
for (auto to : e1[x])
if (to != fa)
dfs(to, x);
dfs2(x, fa, 1, 0);
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d", &c1[i]);
for (int i = 1; i <= m; i++)
scanf("%d", &c2[i]);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
e1[x].push_back(y);
e1[y].push_back(x);
}
for (int i = 1; i < m; i++) {
int x, y;
scanf("%d%d", &x, &y);
e2[x].push_back(y);
e2[y].push_back(x);
}
dfs(1, 0);
printf("%d\n", dp[1][1]);
return 0;
}F
先考虑如何快速做 操作。具体的,考虑如何快速合并两个有序的区间。动态开点值域线段树就非常方便。我们每次可以直接暴力合并。(当然,平衡树也可以)。
所以我们有一个做法,初始每个点都是一个区间,即一棵值域线段树。每次排序一个区间时,把完整在这个区间的进行合并。对于左右两端的,部分在区间内的线段树,我们使用线段树分裂分成两部分然后合并。
一个比较基础的询问就是询问区间和。具体地我们另开一个维护答案的数据结构。对于一整个区间,我们把这个区间的答案放在左端点上。每次询问区间,暴力查询两端散块,在维护答案的数据结构里查询整块答案。
那么对于这道题的询问,考虑没有 操作怎么快速维护。我们在线段树每个区间维护
表示起点奇偶性为
,末尾奇偶性为
的最长答案。合并区间时合并答案。还有一个更方便的方法就是,一个区间
的答案就是这个区间的长度
减去相邻两个数奇偶性相同的个数。把这个东西同样的套到这道题上即可。
初始 个节点,每次分裂产生
个节点 ,时间复杂度
#include <bits/stdc++.h>
#define pi pair<int,int>
#define IT set<int>::iterator
using namespace std;
const int N = 1e5 + 9;
const int M = 1e7 + 9;
int read() {
int f = 0, x = 0;
char ch = getchar();
while (!isdigit(ch)) {
f |= (ch == '-');
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = getchar();
}
return f ? -x : x;
}
int n, m, a[N];
struct point {
int sum;
bool ok, vl, vr;
};
point operator+(point a, point b) {
point res;
res.ok = a.ok | b.ok;
res.sum = a.sum + b.sum;
res.sum += (a.vr == b.vl) && (a.ok && b.ok);
res.vl = a.ok ? a.vl : b.vl;
res.vr = b.ok ? b.vr : a.vr;
return res;
}
struct tree {
#define ls p<<1
#define rs p<<1|1
point t[N << 2];
void add(int p, int l, int r, int nl, int x, pi y) {
if (l == r) {
t[p].ok = 1;
t[p].sum = x;
t[p].vl = y.first;
t[p].vr = y.second;
return;
}
int mid = l + r >> 1;
if (nl <= mid)
add(ls, l, mid, nl, x, y);
else
add(rs, mid + 1, r, nl, x, y);
t[p] = t[ls] + t[rs];
}
void del(int p, int l, int r, int nl) {
if (l == r) {
t[p].ok = 0;
t[p].sum = 0;
t[p].vl = 0, t[p].vr = 0;
return;
}
int mid = l + r >> 1;
if (nl <= mid)
del(ls, l, mid, nl);
else
del(rs, mid + 1, r, nl);
t[p] = t[ls] + t[rs];
}
point get(int p, int l, int r, int nl, int nr) {
if (l >= nl && r <= nr)
return t[p];
int mid = l + r >> 1;
if (nr <= mid)
return get(ls, l, mid, nl, nr);
if (nl > mid)
return get(rs, mid + 1, r, nl, nr);
return get(ls, l, mid, nl, nr) + get(rs, mid + 1, r, nl, nr);
}
#undef ls
#undef rs
} T;
int idx, rt[N];
int ls[M], rs[M], cnt[M], sum[M], tag[N];
bool vl[M], vr[M];
struct Tree {
void push_up(int p) {
sum[p] = sum[ls[p]] + sum[rs[p]];
sum[p] += (vr[ls[p]] == vl[rs[p]]) && (ls[p] && rs[p]);
vl[p] = ls[p] ? vl[ls[p]] : vl[rs[p]];
vr[p] = rs[p] ? vr[rs[p]] : vr[ls[p]];
}
void add(int &p, int l, int r, int k) {
cnt[p = ++idx] = 1;
if (l == r) {
vl[p] = vr[p] = k & 1;
return;
}
int mid = (l + r) >> 1;
k <= mid ? add(ls[p], l, mid, k) : add(rs[p], mid + 1, r, k);
push_up(p);
}
int get(int p, int l, int r) {
if (l == r)
return l;
int mid = (l + r) >> 1;
return ls[p] ? get(ls[p], l, mid) : get(rs[p], mid + 1, r);
}
void merge(int &x, int y) {
if (!(x && y)) {
x |= y;
return;
}
cnt[x] += cnt[y];
merge(ls[x], ls[y]);
merge(rs[x], rs[y]);
push_up(x);
}
void split(int &p1, int p2, int k, int tag) {
if (cnt[p2] == k)
return;
cnt[p1 = ++idx] = cnt[p2] - k;
cnt[p2] = k;
if (tag) {
if (k <= cnt[rs[p2]])
split(rs[p1], rs[p2], k, tag),
ls[p1] = ls[p2], ls[p2] = 0;
else
split(ls[p1], ls[p2], k - cnt[rs[p2]], tag);
} else {
if (k <= cnt[ls[p2]])
split(ls[p1], ls[p2], k, tag),
rs[p1] = rs[p2], rs[p2] = 0;
else
split(rs[p1], rs[p2], k - cnt[ls[p2]], tag);
}
push_up(p1);
push_up(p2);
}
} G;
set<int>t;
IT work(int p) {
auto it = t.lower_bound(p);
if (*it == p)
return it;
--it;
int x = *it;
G.split(rt[p], rt[x], p - x, tag[p] = tag[x]);
T.del(1, 1, n, x);
pi res = make_pair(vl[rt[x]], vr[rt[x]]);
if (tag[p])
swap(res.first, res.second);
T.add(1, 1, n, x, sum[rt[x]], res);
res = make_pair(vl[rt[p]], vr[rt[p]]);
if (tag[p])
swap(res.first, res.second);
T.add(1, 1, n, p, sum[rt[p]], res);
return t.insert(p).first;
}
int main() {
n = read(), m = read();
t.insert(n + 1);
for (int i = 1; i <= n; i++) {
G.add(rt[i], 1, n, a[i] = read());
t.insert(i);
T.add(1, 1, n, i, 0, make_pair(a[i] & 1, a[i] & 1));
}
for (int i = 1; i <= m; i++) {
int op = read() - 1, l = read(), r = read();
if (op < 2) {
auto L = work(l), R = work(r + 1);
for (auto i = ++L; i != R; ++i)
G.merge(rt[l], rt[*i]);
tag[l] = op;
pi res = make_pair(vl[rt[l]], vr[rt[l]]);
if (op)
swap(res.first, res.second);
T.add(1, 1, n, l, sum[rt[l]], res);
for (auto it = L; it != R; ++it)
T.del(1, 1, n, *it);
t.erase(L, R);
} else {
work(l), work(r + 1);
printf("%d\n", r - l + 1 - T.get(1, 1, n, l, r).sum);
}
}
}G
从高到低贪心放 ,那么答案有两种可能:
若 除了最后一位都是
,答案就是
,否则答案是
个
。
#include <bits/stdc++.h>
using namespace std;
int main(){
string s;
cin >> s;
cout << max(s,string( s.size() - 1 ,'9'));
}H
因为限制是对于某一个二进制位,完全背包就很麻烦。这启发考虑按位处理。
具体的,类似于二进制分组优化的想法,把一个物品拆成 份,从小到大枚举二进制位,每一位暴力做
背包。因为做到第
位的时候,加上的数是
的倍数,更低位的数是没有贡献的。于是可以大胆的弃掉。
表示做到第
位,现在背包体积
的方案数。
每次暴力 背包的复杂度显然不能接受。观察每次乘上去的式子
其中
表示二进制第
位被
掉的位置。他也等价于:
$\rm NTT$ 实现。
考虑到 不同的最多只有
个,意思是不同的二项式只有根号个。我们可以暴力求出点值,然后
回去,得到原本的多项式。相比分治
这个非常好写。
然后再暴力除掉下面的每个二项式,这相当于退掉 背包。因为总共的限制只有
个,每次退的时间复杂度是
,和
同阶。所以这部分的时间复杂度是
。
题目要求体积小于等于 的方案数,我们直接乘上一个
转化为求恰好,那么每次进位只能保留和
二进制奇偶相同的体积。即
。进位最多进
,数组只需要开到
即可。
#include <bits/stdc++.h>
using namespace std;
const int p = 998244353;
const int N = 1e5 + 5e4;
const int G = 3, Gi = 332748118;
const int lim = 40001;
int inv(int x, int base = p - 2) {
int res = 1;
while (base) {
if (base & 1)
res = 1ll * res * x % p;
x = 1ll * x * x % p;
base >>= 1;
}
return res;
}
int r[N << 1];
inline int get(int x) {
int len = 1;
while (len <= x)
len <<= 1;
return len;
}
void NTT(int *A, int len, int tag) {
for (int i = 1; i <= len; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) ? (len >> 1) : 0);
for (int i = 0; i < len; i++)
if (i < r[i])
swap(A[i], A[r[i]]);
for (int mid = 1; mid < len; mid <<= 1) {
int wn = inv(tag ? G : Gi, (p - 1) / (mid << 1));
for (int j = 0; j < len; j += (mid << 1))
for (int k = 0, w = 1; k < mid; k++, w = 1ll * w * wn % p) {
int x = A[j + k], y = 1ll * w * A[j + k + mid] % p;
A[j + k] = (x + y) % p;
A[j + k + mid] = (x + p - y) % p;
}
}
if (tag)
return;
int invlen = inv(len);
for (int i = 0; i < len; i++)
A[i] = 1ll * A[i] * invlen % p;
}
int a[N], A[N], F[N], g[N];
int n, K, cnt[N];
long long m;
vector<int>had[62];
int vis[N], tag;
signed main() {
scanf("%d%lld%d", &n, &m, &K);
a[0] = 1;
++cnt[1];
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]), ++cnt[a[i]];
for (int i = 1; i <= K; i++) {
int x, y;
scanf("%d%d", &x, &y);
had[y].push_back(x);
}
int len = get(lim);
int w = inv(3, (p - 1) / len);
A[0] = 1;
for (int i = 1; i < len; i++)
A[i] = 1ll * A[i - 1] * w % p;
for (int i = 0; i < len; i++)
F[i] = 1;
for (int i = 0; i <= lim; i++)
if (cnt[i])
for (int j = 0; j < len; j++)
F[j] = 1ll * F[j] * inv((A[(j * i) & (len - 1)] + 1) % p, cnt[i]) % p;
NTT(F, len, 0);
len = get(2 * lim);
memset(A, 0, sizeof(A));
A[0] = 1;
for (int i = 0; (i <= 60) && m; i++) {
memcpy(g, F, sizeof(F));
++tag;
for (auto x : had[i])
if (vis[x] != tag) {
vis[x] = tag;
for (int j = a[x]; j <= lim; j++)
g[j] = (g[j] + p - g[j - a[x]]) % p;
}
NTT(A, len, 1);
NTT(g, len, 1);
for (int j = 0; j < len; j++)
g[j] = 1ll * g[j] * A[j] % p;
NTT(g, len, 0);
memset(A, 0, sizeof(A));
for (int j = m % 2; j <= 2 * lim; j += 2)
A[j >> 1] = g[j];
m >>= 1;
}
printf("%d", A[0]);
return 0;
}I
题目有个条件就是,初始手牌每一种最多两张。那么最优策略就是,只要新摸到的牌能和手中的单牌凑出对子,就留着,打出一张单牌;否则打出摸到的牌。
那么就可以考虑一个暴力的 表示已经摸了
张牌,手上有
张单牌还需进行的期望轮数。
首先牌库里还有的牌有 张
若摸到的是手中的单牌,期望为 ,否则为
注意,手中的单牌在牌库里一定剩下 张,否则一定在之前凑一起了。
其中手中只剩一张牌并且摸到了需要的要特判转移,或者记忆化搜索。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int p = 1e9 + 7;
int dp[200][14];
int inv(int x, int base = p - 2) {
int res = 1;
while (base) {
if (base & 1)
res = 1ll * res * x % p;
x = 1ll * x * x % p;
base >>= 1;
}
return res;
}
//136-13=123
char s[100];
int sta[100], top;
int dfs(int a, int c) {
if (c == -1) {
return 0;
}
if (dp[a][c] != -1)
return dp[a][c];
int res = 0;
if (123 - a - c * 3)
res += 1ll * (123 - a - c * 3) * inv(123 - a) % p * dfs(a + 1, c) % p;
res += 1ll * (3 * c) * inv(123 - a) % p * dfs(a + 1, c - 2) % p;
return dp[a][c] = (res + 1) % p;
}
signed main() {
int q;
cin >> q;
memset(dp, -1, sizeof dp);
for (int k = 1; k <= q; k++) {
scanf("%s", s + 1);
for (int i = 1; i <= 13; i++)
sta[i] = (s[i * 2 - 1] - '0') * 100 + (s[i * 2] - 'a');
sort(sta + 1, sta + 1 + 13);
int x = 0;
for (int i = 1; i <= 12; i++)
if (sta[i] == sta[i + 1])
x++;
printf("Case #%d: %lld\n", k, (dfs(0, 13 - 2 * x)) % p);
}
return 0;
}
J
首先可以观察出,若两个点的答案集合有交集,则这两个集合一定是包含关系(严谨证明见出题人sol),因此我们可以尝试将每个点的后继合并到这个点,最后输出最大的集合大小。
暴力合并的复杂度显然是无法接受的,因此考虑如下技巧:
若该后继的某个前驱已经在当前点的答案集合,则将该前驱直接从后继的前驱集合中删去。否则说明当前点无法被合并,直接退出循环。最后将当前点插入后继的前驱集合。这样可以避免无意义查询,使得点 的遍历次数是
的。
但是合并后继时貌似有点问题:我们会将后继的后继当成新的后继,若我们构造一个链套菊花图,使得菊花图部分的点都无法被合并但链部分的点都能被合并,则菊花图部分的点会被加入链长次,这样时间复杂度就被卡到了 。
解决方案当然是有的,若我们从链顶开始做,就能避免这一情况,因此我们判断若当前点有唯一前驱且前驱编号比自己大(避免在出现环时出错),则不处理当前点,等到处理前驱时再考虑。
时间复杂度也许是
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
struct dsu {
int fa[N], siz[N];
inline void init(int k) {
for (int i = 1; i <= k; ++i) fa[i] = i, siz[i] = 1;
}
int find(int k) { return k == fa[k] ? k : fa[k] = find(fa[k]); }
inline bool merge(int x, int y) {
x = find(x), y = find(y);
if (x != y) {
fa[x] = y, siz[y] += siz[x];
return true;
}
return false;
}
} d;
int deg[N], n, num, t_case;
set<int> fa[N], to[N];
bool vis[N];
int main() {
scanf("%d", &t_case);
while (t_case--) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) fa[i].clear(), to[i].clear();
for (int i = 1; i <= n; ++i) {
scanf("%d", deg + i);
for (int j = 1, x; j <= deg[i]; ++j) {
scanf("%d", &x);
fa[i].insert(x);
to[x].insert(i);
}
}
int ans = 0;
d.init(n);
for (int i = 1; i <= n; ++i)
if (d.find(i) == i) {
if (fa[i].size() == 1 && *fa[i].begin() > i)
continue;
auto update = [&](int k) {
for (auto it = to[k].begin(); it != to[k].end(); ++it)
if (d.find(*it) != *it)
it = to[k].erase(it);
};
update(i);
queue<int> q;
for (int j : to[i]) q.push(j);
while (q.size()) {
int p = q.front();
q.pop();
if (!to[i].count(p))
continue;
for (auto it = fa[p].begin(); it != fa[p].end();) {
if (d.find(*it) == i)
it = fa[p].erase(it);
else {
to[i].insert(p);
goto skip;
}
}
d.merge(p, i);
to[i].erase(p);
update(p);
for (int j : to[p])
if (i != j)
q.push(j), to[i].insert(j);
skip:;
fa[p].insert(i);
}
ans = max(ans, d.siz[i]);
}
printf("Case #%d: %d\n", ++num, ans);
}
return 0;
}

全部评论
(2) 回帖