NCD2019部分题解
A
解题思路
突破点在二分答案上,如果想到二分的话后面就好做了。
假设我们二分答案的大小为x,判断是否可行,首先肯定需要在长度不小于2x的线段中找。考虑枚举竖线来找符合条件的横线,对于一条竖线\({x_1, y_1, c_1}(x_1 \leq y_1)\)来说,需要判断是否存在一条横线\({x_2, y_2, c_2}(x_2\leq y_2)\),满足\(x_1+x \leq c_2 \leq y_1-x\)和\(x_2+x \leq c_1 \leq y_2-x\),我们的问题有两个维度,如果想同时判断的话十分麻烦,可以考虑如何消去其中一维的影响,考虑另一维。
这里有一个经典套路,我们发现对于横坐标x,一共分为三类,一类是横线的左端点,一类是横线的右端点,还有一类是竖线的横坐标。我们对于所有横坐标以及种类进行双关键字排序,从小到大遍历这些横坐标,如果当前横坐标是左端点,我们就把它对应的横线的纵坐标加进集合里,如果是右端点,因为横坐标是递增的,那么这个点对应的横线必定和后面的竖线没有交叉,将其从集合中删去,如果是竖线的横坐标,那我们就从集合里找一个大于等于竖线较小的纵坐标的最小的点,如果这个点小于竖线的较大的那个纵坐标,就说明有解。
代码
struct INFO {
int a, b, c, tp;
} sgx[maxn], sgy[maxn];
int n, m;
int check(int x) {
int t1 = 0, t2 = 0;
vector<INFO> sg;
for (int i = 1; i<=n; ++i) {
if (sgx[i].b-sgx[i].a<2*x) continue;
sg.push_back({sgx[i].a+x, 0, sgx[i].c, 1});
sg.push_back({sgx[i].b-x, 0, sgx[i].c, 3});
}
for (int i = 1; i<=m; ++i) {
if (sgy[i].b-sgy[i].a<2*x) continue;
sg.push_back({sgy[i].c, sgy[i].a+x, sgy[i].b-x, 2});
}
multiset<int> st;
sort(sg.begin(), sg.end(), [](INFO A, INFO B) {return A.a==B.a ? A.tp<B.tp:A.a<B.a;});
for (auto v : sg) {
if (v.tp==1) st.insert(v.c);
else if (v.tp==3) st.erase(st.find(v.c));
else {
auto it = st.lower_bound(v.b);
if (it!=st.end() && *it<=v.c) return 1;
}
}
return 0;
}
int main() {
IOS;
int __; cin >> __;
while(__--) {
cin >> n >> m;
for (int i = 1; i<=n; ++i) {
cin >> sgx[i].a >> sgx[i].b >> sgx[i].c;
if (sgx[i].a>sgx[i].b) swap(sgx[i].a, sgx[i].b);
}
for (int i = 1; i<=m; ++i) {
cin >> sgy[i].a >> sgy[i].b >> sgy[i].c;
if (sgy[i].a>sgy[i].b) swap(sgy[i].a, sgy[i].b);
}
int l = 0, r = 1e5;
while(l<r) {
int mid = (l+r+1)>>1;
if (check(mid)) l = mid;
else r = mid-1;
}
cout << l << endl;
}
return 0;
}
B
解题思路
不难看出,能抓住Hasan的边是割边,而别的边没有影响。我们先对图中的所有连通块中的边双连通分量进行缩点,缩点后的图就变成了一片森林,我们在森林中的两棵树中连边,会变成一棵树,而割边数量会加1,如果我们对其中的一棵树上的两点连一条边,会发现两点简单路径上的所有点都会变成一个新的边双连通分量,减少的割边数量就是他们的最短距离,所以我们需要找到每棵树上任意两点的最长的最短距离,即树的直径,然后用割边数量-树的直径即为答案。
代码
const int maxn = 1e6+10;
const int maxm = 1e6+10;
struct E {
int to, nxt;
} e[maxm];
int h[maxn], tot = 1;
void add(int u, int v) {
e[++tot] = {v, h[u]};
h[u] = tot;
}
int n, m;
int dfn[maxn], low[maxn], __dfn;
int sk[maxn], tp, dcc, id[maxn];
int isbrige[maxn];
vector<int> g[maxn];
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++__dfn;
sk[++tp] = u;
for (int i = h[u]; i; i = e[i].nxt) {
if ((fa^1)==i) continue;
int v = e[i].to;
if (!dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v]>dfn[u]) isbrige[i] = isbrige[i^1] = 1;
}
else low[u] = min(low[u], dfn[v]);
}
if (low[u]==dfn[u]) {
int v; ++dcc;
do {
v = sk[tp--];
id[v] = dcc;
} while(v!=u);
}
}
int maxx, vis[maxn];
int dfs(int u) {
vis[u] = 1;
int f = 0;
for (auto v : g[u]) {
if (vis[v]) continue;
int dis = dfs(v)+1;
maxx = max(maxx, f+dis);
f = max(f, dis);
}
return f;
}
void init() {
__dfn = tp = dcc = 0;
clr(isbrige, 0);
tot = 1;
for (int i = 0; i<=n; ++i) {
h[i] = vis[i] = dfn[i] = low[i] = sk[i] = id[i] = 0;
g[i].clear();
}
}
int main() {
IOS;
int __; cin >> __;
while(__--) {
cin >> n >> m;
init();
for (int i = 1, a, b; i<=m; ++i) {
cin >> a >> b;
add(a, b); add(b, a);
}
for (int i = 1; i<=n; ++i)
if (!dfn[i]) tarjan(i, -1);
int ans = 0;
for (int i = 1; i<=n; ++i)
for (int j = h[i]; j; j = e[j].nxt) {
int v = e[j].to;
if (isbrige[j]) g[id[i]].push_back(id[v]), ++ans;
}
ans /= 2;
maxx = 0;
for (int i = 1; i<=dcc; ++i)
if (!vis[i]) dfs(i);
ans -= maxx;
cout << ans << endl;
}
return 0;
}
C
解题思路
这题也是经典套路了,考虑\(O(n^2)\)的做法,设dp[j]为以j结尾的lis长度,我们在遍历前面比当前a[i]小的数a[j]的时候,如果dp[j]可以更新答案,那么方案数同dp[j]的方案数,如果dp[j]+1和dp[i]相同,那么方案数就可以累加。
代码
const int maxn = 2e5+10;
const int maxm = 2e6+10;
int a[maxn], dp[maxn], cnt[maxn];
int main() {
IOS;
int __; cin >> __;
while(__--) {
int n; cin >> n;
for (int i = 1; i<=n; ++i) cin >> a[i];
int maxx = 0, sum = 0;
for (int i = 1; i<=n; ++i) {
cnt[i] = dp[i] = 1;
for (int j = i-1; j>=1; --j) {
if (a[j]<a[i]) {
if (dp[i]<dp[j]+1) {
dp[i] = dp[j]+1;
cnt[i] = cnt[j];
}
else if (dp[i]==dp[j]+1) {
cnt[i] = (cnt[i]+cnt[j])%MOD;
}
}
}
if (dp[i]>maxx) maxx = dp[i], sum = cnt[i];
else if (dp[i]==maxx) sum = (sum+cnt[i])%MOD;
}
cout << maxx << ' ' << sum << endl;
}
return 0;
}
D略
E
解题思路
如果没有修改操作只有询问的话,我们求正反两个字符串的哈希值就能做,如果加上修改操作,比如i的位置修改一下,对于正的字符串来说,i到n的哈希值会改变,对于颠倒后的字符串的哈希值来说,n-i+1到n的哈希值会改变。
如果是按照常规的计算哈希值的方法的话,我们就是对区间修改一个变量,这样维护起来十分的痛苦。我的方法是把每一个哈希值都往后补0,使得每一个哈希值对应的都是P进制下的一个n位整数,这样带来的好处是,由于长度是固定的,那么修改i的值之后,i到n这个区间上改变的值也是一样的,我们区间修改的就是一个常量了。这样我们只要写一个区间修改,单点询问的线段树。修改操作就不说了,对于查询,我们用r对应的值减去l-1对应的值,再去掉末尾0,得到一段字符串的哈希值,然后用类似的方法求出颠倒后的区间的哈希值,两者比较即可。
const int maxn = 1e5+10;
const int maxm = 2e6+10;
ll lz[maxn<<2][2], tr[maxn<<2][2], f[maxn], h[maxn][2];
inline void push_down(int rt, int s) {
if (lz[rt][s]) {
tr[rt<<1][s] = (tr[rt<<1][s]+lz[rt][s]+MOD)%MOD;
tr[rt<<1|1][s] = (tr[rt<<1|1][s]+lz[rt][s]+MOD)%MOD;
lz[rt<<1][s] = (lz[rt<<1][s]+lz[rt][s]+MOD)%MOD;
lz[rt<<1|1][s] = (lz[rt<<1|1][s]+lz[rt][s]+MOD)%MOD;
lz[rt][s] = 0;
}
}
void build(int rt, int l, int r, int s) {
tr[rt][s] = lz[rt][s] = 0;
if (l==r) {
tr[rt][s] = h[l][s];
return;
}
int mid = (l+r)>>1;
build(rt<<1, l, mid, s);
build(rt<<1|1, mid+1, r, s);
}
void update(int rt, int l, int r, int L, int R, int V, int s) {
if (l>=L && r<=R) {
tr[rt][s] = (tr[rt][s]+V+MOD)%MOD;
lz[rt][s] = (lz[rt][s]+V+MOD)%MOD;
return;
}
push_down(rt, s);
int mid = (l+r)>>1;
if (L<=mid) update(rt<<1, l, mid, L, R, V, s);
if (R>mid) update(rt<<1|1, mid+1, r, L, R, V, s);
}
ll ask(int rt, int l, int r, int pos, int s) {
if (!pos) return 0;
if (l==r) return tr[rt][s];
push_down(rt, s);
int mid = (l+r)>>1;
if (pos<=mid) return ask(rt<<1, l, mid, pos, s);
else return ask(rt<<1|1, mid+1, r, pos, s);
}
void ck(int rt, int l, int r, int s) {
if (l==r) {
cout << tr[rt][s] << ' ';
return;
}
push_down(rt, s);
int mid = (l+r)>>1;
ck(rt<<1, l, mid, s);
ck(rt<<1|1, mid+1, r, s);
}
char str[maxn], str2[maxn];
int n, m;
void change(int pos, ll ff) {
ll x = 1ll*(str[pos])*f[n-pos]%MOD;
update(1, 1, n, pos, n, MOD+ff*x, 0);
int rpos = n-pos+1;
ll y = 1ll*(str2[rpos])*f[n-rpos]%MOD;
//cout << x << ' ' << y << endl;
update(1, 1, n, rpos, n, MOD+ff*y, 1);
}
ll qp(ll x, ll y) {
x %= MOD;
ll res = 1;
while(y) {
if (y&1) res = res*x%MOD;
x = x*x%MOD;
y >>= 1;
}
return res;
}
int main() {
IOS;
f[0] = 1;
for (int i = 1; i<maxn; ++i) f[i] = f[i-1]*P%MOD;
int __; cin >> __;
while(__--) {
cin >> n >> m;
cin >> str+1;
for (int i = 1; i<=n; ++i) str2[i] = str[n-i+1];
for (int i = 1; i<=n; ++i) h[i][0] = (h[i-1][0]+1ll*(str[i])*f[n-i])%MOD;
for (int i = 1; i<=n; ++i) h[i][1] = (h[i-1][1]+1ll*(str2[i])*f[n-i])%MOD;
build(1, 1, n, 0);
build(1, 1, n, 1);
int op, l, r, pos; char ch[10];
while(m--) {
cin >> op;
if (op==1) {
cin >> pos >> ch;
change(pos, -1);
str[pos] = ch[0];
str2[n-pos+1] = ch[0];
change(pos, 1);
}
else {
cin >> l >> r;
ll A = (ask(1, 1, n, r, 0)-ask(1, 1, n, l-1, 0)+MOD)%MOD;
//cout << A << endl;
A = A*qp(f[n-r], MOD-2)%MOD;
A = (A%MOD+MOD)%MOD;
l = n-l+1, r = n-r+1;
if (l>r) swap(l, r);
//cout << l << ' ' << r << endl;
ll B = (ask(1, 1, n, r, 1)-ask(1, 1, n, l-1, 1)+MOD)%MOD;
B = B*qp(f[n-r], MOD-2)%MOD;
B = (B%MOD+MOD)%MOD;
//cout << A << ' ' << B << endl;
if (A==B) cout << "Adnan Wins" << endl;
else cout << "ARCNCD!" << endl;
}
//ck(1, 1, n, 0); cout << endl;
//ck(1, 1, n, 1); cout << endl;
//cout << "!" << endl;
}
}
return 0;
}
F略
G
(代补)
H略
J
解题思路
很简单的题,题目就两种操作,一种区间加1,一种询问区间出现最多的数,而且就一个询问,直接差分数组做就行了。
const int maxn = 1e6+10;
const int maxm = 1e6+10;
int a[maxn], b[maxn], sub[maxn];
int main() {
IOS;
int __; cin >> __;
while(__--) {
int n, m; cin >> n >> m;
for (int i = 0; i<=n; ++i) sub[i] = 0;
for (int i = 1; i<=m; ++i) cin >> a[i], --a[i];
for (int i = 1; i<=m; ++i) {
cin >> b[i];
if (abs(b[i])==n) {
++sub[a[i]];
--sub[a[i]+1];
++sub[0];
--sub[n];
}
else if (b[i]>=0) {
++sub[a[i]];
--sub[min(n, a[i]+b[i]+1)];
if (a[i]+b[i]>=n) {
b[i] = (a[i]+b[i])%n;
++sub[0];
--sub[b[i]+1];
}
}
else if (b[i]<0) {
++sub[max(0, a[i]+b[i])];
--sub[a[i]+1];
if (a[i]+b[i]<0) {
b[i] = (a[i]+b[i])+n;
++sub[b[i]];
--sub[n];
}
}
}
for (int i = 1; i<=n; ++i) sub[i] += sub[i-1];
int c = 0;
for (int i = 0; i<n; ++i) {
if (sub[i]>sub[c]) c = i;
//cout << sub[i] << endl;
}
cout << c+1 << ' ' << sub[c] << endl;
}
return 0;
}
K略
L略
M
经典取log,需要注意底数为0的时候会返回-inf,而两个-inf不相等