DMY 周作业 46 简要题解
D
考虑枚举两个区间最终的 \(\text{MEX}\),发现固定 \(\text{MEX} = x\) 时,所有 \(< x\) 的数都要被包含,所有 \(=x\) 的数都不能被包含。合法区间 \((l, r)\) 相当于一个二维平面上的矩形,求其面积即可。
时间复杂度 \(O(n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 200005, inf = 0x3f3f3f3f;
int n, a[N], b[N], posa[N], posb[N];
ll ans;
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
posa[a[i]] = i;
}
for(int i = 1; i <= n; i++)
{
cin >> b[i];
posb[b[i]] = i;
}
int prela = inf, prera = -inf, prelb = inf, prerb = -inf;
int tmplen = max(posa[1], posb[1]) - min(posa[1], posb[1]) - 1;
ans = 1 + 1ll * tmplen * (tmplen + 1) / 2;
tmplen = min(posa[1], posb[1]) - 1;
ans += 1ll * tmplen * (tmplen + 1) / 2;
tmplen = n - max(posa[1], posb[1]);
ans += 1ll * tmplen * (tmplen + 1) / 2;
prela = min(prela, posa[1]);
prera = max(prera, posa[1]);
prelb = min(prelb, posb[1]);
prerb = max(prerb, posb[1]);
for(int i = 2; i <= n; i++)
{
int la = prela, lb = prelb, ra = prera, rb = prerb;
int lx = min(prela, prelb);
int rx = max(prera, prerb);
if(!(lx <= posa[i] && posa[i] <= rx) && !(lx <= posb[i] && posb[i] <= rx))
{
int lt = 1, rt = n;
if(posa[i] < lx) lt = max(lt, posa[i] + 1);
else rt = min(rt, posa[i] - 1);
if(posb[i] < lx) lt = max(lt, posb[i] + 1);
else rt = min(rt, posb[i] - 1);
ans += 1ll * (lx - lt + 1) * (rt - rx + 1);
}
prela = min(prela, posa[i]);
prera = max(prera, posa[i]);
prelb = min(prelb, posb[i]);
prerb = max(prerb, posb[i]);
}
cout << ans;
return 0;
}
E
Sol.1
对深度启发式合并,然后随便做。时间复杂度 \(O(n\log n)\)。
Sol.2
对每个节点的每个深度分别考虑,发现有贡献的情况始终有 \(2^{n - 1}\) 种。具体而言,对于子树内特定层的节点,假设这样的节点有 \(k\) 个,则一共有 \(2^{k - 1}\) 种方案(可以通过二项式定理并错位相减、计数转概率、对称性构造等多种方式证明);对于不在子树内特定层的节点,怎么染色都没有影响,方案数为 \(2^{n - k}\)。
于是只需要求每个点子树内的最大深度即可。时间复杂度 \(O(n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 200005;
const ll mod = 1e9 + 7;
int n;
ll ans, pw, dp[N];
vector<int> g[N];
void dfs(int u, int fa)
{
for(auto v : g[u])
{
if(v == fa) continue;
dfs(v, u);
dp[u] = max(dp[u], dp[v]);
}
dp[u]++;
ans = (ans + dp[u] * pw % mod) % mod;
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
g[i].clear();
dp[i] = 0;
}
pw = 1;
for(int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
pw = (pw * 2) % mod;
}
ans = 0;
dfs(1, 0);
cout << ans << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--) solve();
return 0;
}
F
800 题,建议出题人少玩原神。
静态问题是经典的插入 DP,考虑每个元素插入时在集合中的排名,两个方案不同当且仅当某次插入的排名不同。因此 \(\texttt{<,>}\) 给答案的系数为 \(1\),\(\texttt{?}\) 给答案的系数为 \(i - 1\)。这个可以直接动态维护。注意特判第一个位置不能为 \(\texttt{?}\),否则无解。
线性预处理逆元,时间复杂度 \(O(n)\)。
Bonus:如果模数不为质数,则可能出现不存在逆元的情况。此时可以使用线段树进行维护不可减信息。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 300005;
const ll mod = 998244353;
int n, q;
ll inv[N], ans = 1;
char s[N];
void outp()
{
if(s[1] == '?') cout << "0\n";
else cout << ans << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> q >> s + 1;
inv[1] = inv[0] = 1;
for(int i = 2; i < N; i++)
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for(int i = 2; i < n; i++)
if(s[i] == '?') ans = (ans * (i - 1)) % mod;
outp();
while(q--)
{
int x; char c;
cin >> x >> c;
if(s[x] == '?') ans = (ans * inv[x - 1]) % mod;
s[x] = c;
if(s[x] == '?' && x != 1) ans = (ans * (x - 1)) % mod;
outp();
}
return 0;
}
I
Sol.1
没有利用任何 \(\varphi\) 的性质。先线性筛求出 \(\varphi\) 的表,建树,然后容易发现操作 \(2\) 等价于求 \(\sum_{u =l}^r dep_u - dep_{\text{LCA}}\)。
维护 \(\sum dep\) 是容易的。只需要考虑如何求 \(dep_{\text{LCA}}\) 即可。
根据树上查询的经典结论,我们求任意相邻两个节点的 \(\text{LCA}\),区间内任意两个相邻节点 \(\text{LCA}\) 的深度的最小值即为整个区间 \(\text{LCA}\) 的深度。
向上跳节点分为两部分,第一部分是两个都没有跳到 \(\text{LCA}\) 处,\(\text{LCA}\) 不变。第二部分是一个跳到了 \(\text{LCA}\) 处,此时深度更浅的那个节点就是 \(\text{LCA}\)。
可能要用吉司机维护 \(\sum dep\),因为要始终保证 \(dep\ge 1\)。时间复杂度 \(O(n\log^2n)\)。
Sol.2
延续 Sol.1 的做法。但是不用树上查询的结论。
结论 \(1\):一个点集的 \(\text{LCA}\),为 \(dfn\) 最小的点,与 \(dfn\) 最大的点的 \(\text{LCA}\)。
可能还是要吉司机。时间复杂度 \(O(n\log^2n)\)。
Sol.3
结论 \(2\):值为 \(V\) 的数,每次操作后变为 \(\varphi(V)\),最多执行 \(O(\log V)\) 次操作就能回到 \(1\)。
于是直接势能线段树维护即可。
时间复杂度 \(O(n\log n\log V)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc (p << 1)
#define rc ((p << 1) | 1)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 100005, V = 5000005;
int n, q, cnt, a[N], prm[V], phi[V], b[N];
bitset<V> vis;
struct Edge{
int v, ne;
} e[V];
int h[V], idx;
void add(int u, int v)
{
e[++idx] = {v, h[u]};
h[u] = idx;
}
void seive()
{
vis[0] = vis[1] = phi[1] = 1;
for(int i = 2; i < V; i++)
{
if(!vis[i])
{
prm[++cnt] = i;
phi[i] = i - 1;
}
for(int j = 1; j <= cnt && i * prm[j] < V; j++)
{
int v = i * prm[j];
vis[v] = 1;
if(i % prm[j]) phi[v] = (prm[j] - 1) * phi[i];
else
{
phi[v] = prm[j] * phi[i];
break;
}
}
add(phi[i], i);
}
}
int fa[V], dep[V], top[V], son[V], sz[V];
void dfs1(int u, int father)
{
fa[u] = father; dep[u] = dep[fa[u]] + 1; sz[u] = 1;
for(int i = h[u]; i ; i = e[i].ne)
{
int v = e[i].v;
if(v == fa[u]) continue;
dfs1(v, u);
sz[u] += sz[v];
if(sz[son[u]] < sz[v]) son[u] = v;
}
}
void dfs2(int u, int tp)
{
top[u] = tp;
if(!son[u]) return;
dfs2(son[u], tp);
for(int i = h[u]; i ; i = e[i].ne)
{
int v = e[i].v;
if(v == fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
int getlca(int u, int v)
{
while(top[u] != top[v])
{
if(dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
if(dep[u] < dep[v]) swap(u, v);
return v;
}
struct Node{
ll l, r, mnv, mndep, mxdep, smdep;
};
struct Segtree{
Node tr[4 * N];
void pushup(Node &p, Node ls, Node rs)
{
p.mnv = min(ls.mnv, rs.mnv);
p.mndep = min(ls.mndep, rs.mndep);
p.mxdep = max(ls.mxdep, rs.mxdep);
p.smdep = ls.smdep + rs.smdep;
}
void build(int p, int ln, int rn)
{
tr[p] = {ln, rn, b[ln], dep[a[ln]], dep[a[ln]], dep[a[ln]]};
if(ln == rn) return;
int mid = (ln + rn) >> 1;
build(lc, ln, mid);
build(rc, mid + 1, rn);
pushup(tr[p], tr[lc], tr[rc]);
}
void update(int p, int ln, int rn)
{
if(tr[p].mxdep == 1) return;
if(tr[p].l == tr[p].r)
{
tr[p].mndep--;
tr[p].mxdep--;
tr[p].smdep--;
return;
}
int mid = (tr[p].l + tr[p].r) >> 1;
if(ln <= mid) update(lc, ln, rn);
if(rn >= mid + 1) update(rc, ln, rn);
pushup(tr[p], tr[lc], tr[rc]);
}
Node query(int p, int ln, int rn)
{
if(ln <= tr[p].l && tr[p].r <= rn) return tr[p];
int mid = (tr[p].l + tr[p].r) >> 1;
if(rn <= mid) return query(lc, ln, rn);
if(ln >= mid + 1) return query(rc, ln, rn);
Node tmp;
pushup(tmp, query(lc, ln, rn), query(rc, ln, rn));
return tmp;
}
} tr1;
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
seive();
dfs1(1, 0);
dfs2(1, 1);
cin >> n >> q;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
if(i > 1) b[i - 1] = dep[getlca(a[i], a[i - 1])];
}
tr1.build(1, 1, n);
while(q--)
{
int opt, l, r;
cin >> opt >> l >> r;
if(opt == 1)
tr1.update(1, l, r);
else
{
Node res = tr1.query(1, l, r);
ll smdep = res.smdep, mndep = res.mndep;
if(l <= r - 1) mndep = min(mndep, tr1.query(1, l, r - 1).mnv);
cout << smdep - mndep * (r - l + 1) << "\n";
}
}
return 0;
}
J
很快就想到了钦定最小值然后状压,但是没注意到每个颜色连续成段,于是糊了个神秘 \(O(n\log^2n + 2^mm\log n)\) 的 ST 表二分 + 状压的小丑做法,喜提 TLE #9。卡常一晚上拼尽全力无法通过。
注意到每个组的人数限制与其最小值有关,于是容易想到钦定最小值的做法。\(m\le20\) 启示我们可以使用状压来表示每个组的最小值是否已经被钦定。
注意到随着一个组的最小值不断变小,其限制的最小人数不断增加,但是始终可以找到一个最大的最小值 \(A_j\) 使得 \(pre + 1\sim j\) 里全是一个组。于是可以将 \(A\) 从大到小排序后 DP:\(dp_{msk}\) 表示当前已钦定的组为 \(msk\),需要的 \(A\) 的最小前缀长度。这个 DP 同样可以先暴力设完状态后,通过交换维度理解。
如果是刷表法转移,则可以转化为对于每一个 \((i, j)\),找到最小的 \(k\),使得 \(i + size_{j, k} \le k\)。其中 \(i\) 表示当前要转移的 DP 值,\(j\) 表示转移的组的编号,\(k\) 表示转移后的 DP 值。因为 \(size_{j, k}\) 单调不降,于是可以对每个组预处理,双指针维护最小的 \(k\)。这也侧面印证了每个颜色连续成段的结论。
时间复杂度 \(O(nm+2^mm)\)。构造方案则直接在转移的时候记录转移前驱即可。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 200005, M = 22, V = 1100005, inf = 0x3f3f3f3f;
int n, m, a[N], b[N], mnsz[M][N], orip[N], mnto[M][N], dp[V];
pi frm[V], oris[N];
bool cmp(int x, int y)
{
return (x > y);
}
bool cmp2(pi x, pi y)
{
return (x > y);
}
void upd(int id, int x, pi pre)
{
if(x < dp[id])
{
dp[id] = x;
frm[id] = pre;
}
}
vector<int> choose[M];
int main()
{
// freopen("CF1886E.in", "r", stdin);
// freopen("CF1886E.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
oris[i] = {a[i], i};
}
for(int i = 1; i <= m; i++) cin >> b[i];
sort(a + 1, a + n + 1, cmp);
sort(oris + 1, oris + n + 1, cmp2);
for(int i = 1; i <= n; i++) orip[i] = oris[i].se;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
mnsz[j][i] = ((b[j] + a[i] - 1) / a[i]);
for(int i = 1; i <= m; i++)
{
int p = 1;
for(int j = 0; j <= n; j++)
{
while(p <= n && (p <= j || j > p - mnsz[i][p])) p++;
mnto[i][j] = p;
}
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for(int i = 0; i < (1 << m) - 1; i++)
{
if(dp[i] >= n) continue;
for(int j = 1; j <= m; j++)
{
if((i >> (j - 1)) & 1) continue;
int v = (i | (1 << (j - 1)));
upd(v, mnto[j][dp[i]], make_pair(dp[i], j));
}
}
if(dp[(1 << m) - 1] > n)
{
cout << "NO";
return 0;
}
cout << "YES\n";
int now = (1 << m) - 1, ncnt = 0;
while(now)
{
pi fv = frm[now];
int presz = fv.fi, prex = fv.se;
for(int i = presz + 1; i <= dp[now]; i++)
choose[prex].push_back(orip[i]);
now ^= (1 << (prex - 1));
}
for(int i = 1; i <= m; i++)
{
cout << choose[i].size() << " ";
for(auto itm : choose[i]) cout << itm << " ";
cout << "\n";
}
return 0;
}

浙公网安备 33010602011771号