做题记录啦啦啦
做题记录?
E. Deconstruction Tree
小清新题目。
一棵有 \(n\) 个节点的树从天而降,同时掉落的还有一个初始空集 \(S\)。你对这一不可能发生的事件欣喜若狂,于是做了以下 \(n - 1\) 次:
- 让 \(x\) 成为索引最大的叶子。
- 将 \(x\) 添加到 \(S\) 中(注意,如果 \(x\) 已经在 \(S\) 中,则不会有任何变化)。
- 选择除 \(x\) 以外的任何叶子,并将其从树中删除。
确定可以组成的不同集合 \(S\) 的个数。由于这个数字可能非常大,因此输出它的模数 \(998\,244\,353\)。
做法
先手玩一下整个过程。
发现集合添加的数是单调递增的。
因此可以把整个过程看成是每次选入一个值进集合。
当我们想添加一个数的时候,我们需要将它的度数删到 \(1\)。
并且删的数小于集合的最大值。
我们只关心选的值,容易发现中间的过程是没有影响的。
不妨令 \(n\) 为根,这样添加 \([1,n-1]\) 的点只需要删去它们的子树。
设 \(sub_x\) 为 \(x\) 的子树除 \(x\) 以外的最大值,\(y\) 为集合的最大值。
\(x\) 可选满足 \(sub_x < y < x\)。\(sub_n\) 需要另外算一下。
于是我们得到了一个 dp 做法。
设 \(f_x\) 表示一直集合最大值为 \(x\) 的方案数。
有:\(f_x = \sum_{sub_x+1}^{x-1} f_y\)。
随便树状数组或者差分一下就能得到 \(O(n\log n)\) 或者 \(O(n)\) 的做法。
CF2231E Graph Cutting 题解
一个简单自然的 \(O(n^2)\) 树上背包做法。
直接考虑枚举中心点。
转化为要选三棵子树,每棵子树内选一个点的深度之和为 \(D-1\) 的方案数。
考虑朴素背包。
令 \(f_{c,x}\) 为选 \(c\) 个点,深度之和为 \(x\) 的方案数。
枚举中心点,转移就每次合并一棵子树进去。
但是这样的复杂度是 \(O(n^3)\) 的。最坏可以考虑一条链的情况。
想想看怎么优化。
注意到题目只需要求深度之和为 \(D\) 的方案数。
即我们并不需要知道合并子树出来的整个 dp 数组。
根据树上背包的经典均摊结论,两个背包合代价为 \(O(siz_u \times siz_v)\),则总时间复杂度为 \(O(n^2)\)。
也就是说我们其实可以知道每个点子树内的 \(f_{c,x}\)。
那我们就可以将子树内与子树外在 \(O(D)\) 的时间复杂度内拼出答案:
rep(o,0,D-2) ans += d[o] * f[x][D-o-1];
CF1781F Bracket Insertion
简单记录下。
初始空串,每次概率 “()” “)(” ,问 \(n\) 次还合法的概率。
直接手玩括号折线图,考虑前缀和数组。
我们发现把前缀和作为计数对象是关键的。位置并不重要。
问题转化为:给定一个集合,初始为 {0},每次随机选择一个元素 x,有 p 的概率将 x+1,x 加入集合,有 1−p 的概率将 x−1,x 加入集合,求最终所有元素非负的概率。
概率转方案数。
我们最后不是求有多少种合法的括号串,而是求出所有的 1×3×⋯×(2n−1) 种插入空格的方式中,每种方式最后合法的概率之和
总方案数 \((2n -1)!!\)
\(f(n,x)\) 表示初始有 \(x\) ,\(n\) 次操作后仍合法的合法操作集合方案数。
转移就考虑得到 \(x,x,x+1\) .
事实上它们之间的操作是独立的
分别分配几次操作次数进去。
得到类似:
再随便分离一下 \(i,j\) 可以得到 \(O(n^3)\) 做法。
CF1322C Instant Noodless
题目描述
Wu 在一次激烈的训练后感到饥饿,来到附近的商店购买他最喜欢的方便面。在 Wu 付款后,收银员给了他一个有趣的任务。
给定一个二分图,右半部分的所有顶点上都标有正整数。对于左半部分顶点的一个子集 \(S\),定义 \(N(S)\) 为所有与 \(S\) 中至少一个顶点相邻的右半部分顶点的集合,\(f(S)\) 为 \(N(S)\) 中所有顶点上的数之和。请你求出所有可能的非空子集 \(S\) 的 \(f(S)\) 的最大公约数
做法
注意到
因为惊人的注意到:\(gcd(a,b,c) = gcd(a,b,ma + nb \pm c)\)。
可以根据 gcd 的结合律和更相减损术推出。
那么每次加集合就等价于在原有集合中划分出来一点。
变成若干集合。
\(u\) 和 \(v\) 两个点位于同一自由集合当且仅当 $ T_u = T_v$
做完了。\(T_x\) 是 \(x\) 作为右部点对应的左部点集合。
CF1338D Nested Rubber Bands
题目描述
你有一棵包含 \(n\) 个顶点的树。你需要将这棵树转换为无限大平面上的 \(n\) 个橡皮筋。转换规则如下:
- 对于每一对顶点 \(a\) 和 \(b\),当且仅当树中存在一条连接 \(a\) 和 \(b\) 的边时,橡皮筋 \(a\) 和橡皮筋 \(b\) 必须相交。
- 每根橡皮筋的形状必须是一个简单环。换句话说,橡皮筋是一个不自交的环。
现在定义如下概念:
- 如果橡皮筋 \(a\) 包含橡皮筋 \(b\),当且仅当橡皮筋 \(b\) 在橡皮筋 \(a\) 的区域内部,且它们不相交。
- 一组橡皮筋 \(a_{1}, a_{2}, \ldots, a_{k}\)(\(k \ge 2\))是嵌套的,当且仅当对于所有 \(i\)(\(2 \le i \le k\)),都有 \(a_{i-1}\) 包含 \(a_{i}\)。
做法
考虑这种包含关系会怎么产生冲突。
其一显然是必须得是独立集.
对于第二个限制,与 a,c 相交的极小的“顺次连接的环”实际上由 a 到 c 路径上的所有点构成,因此只有当 b 到路径 (a,c) 的距离小于等于 1 的时候才合法。
所以对于合法序列 a1,a2,a3,…a**k,必有 a**i 到路径 (a1,a**k) 的距离小于等于 1,换言之,这些点一定是树上的一个导出“毛毛虫”的独立集。
CF2229F Load Unbalancing
好题。
题意
已给出一个长度为 \(n\) 的序列 \(a\),以及一个整数 \(k\)。
你可以对 \(a\) 的元素重新排序任意次。然后,定义 \(f(a)\) 如下:
- 设 \(b\) 为长度为 \(k\) 且初始值均为零的数组。
- 从 \(1 \le i \le n\) 顺序处理,向 \(b\) 中的任何最小元素添加 \(a_i\)。
- 最后,\(f(a) = \max(b)\)。
求出重排 \(a\) 后 \(f(a)\) 的可能最大值。
过程比较复杂难考虑,我们直接考虑对于最后 \(a_i\) 的一组分组合法的充要。
设和最大的那一堆是 \(A\)。
手玩发现是 \(A\) 堆减去 \(A_{max}\) 一定小于等于所有其他堆。
可以证明 \(A_{max}\) 是最大值一定最优。
假设现在有一组最优的分组最大值不在最后一个,我们把最后一个塞进最大值所在堆,将最大值拎出来。
容易发现此时最大值塞入最小堆后不劣。
转化问题
\(17\) 个数,值域 \(1e9\),分成 \(k\) 组,最小值最大怎么做?
如果是在序列上分组咋做?
简单二分 + 贪心。
我们这里可以移用这个做法。
即考虑排列这些数,然后在上面贪心。
在 \(n\) 较小的情况下排列容易想到状压 dp。
先二分答案 \(\ge k\)。
我们需要将贪心所需条件记下来。
于是我们可以设一个 \(f_{st}\) 表示在把 \(st\) 集合的数塞进去后,能分多少组,余数是多少。
对这两个数值做双关键字比较即可。
CF1355F Guess Divisors Count
神秘交互
我们有一个隐藏的整数 \(1 \le X \le 10^{9}\)。你不需要猜出这个数本身。你需要找出这个数的约数个数,甚至不需要精确找出:只要你的答案的绝对误差不超过 7,或者相对误差不超过 \(0.5\),你的答案就会被认为是正确的。更正式地说,设你的答案为 \(ans\),\(X\) 的约数个数为 \(d\),那么只要以下两个条件之一成立,你的答案就被认为是正确的:
- \(|ans - d| \le 7\);
- \(\frac{1}{2} \le \frac{ans}{d} \le 2\)。
你最多可以进行 \(22\) 次询问。每次询问你可以给出一个整数 \(1 \le Q \le 10^{18}\)。作为回应,你会得到 \(gcd(X, Q)\)——即 \(X\) 和 \(Q\) 的最大公约数。
先分析第二个限制,即我们的 \(res \in [\frac{ans}{2},2\times ans]\)。
注意到 [1e3,1e9] 中不会超过 2 个质因数。
假如我们知道前面的约数个数,则我们输出 \(res\times 2\) 即可。因为后面只可能 \(x1,2,3,4\)。
但是发现不够,分组查最多查到700.
考虑第一个限制。
当答案很小的时候考虑使用。
当 \(res = 1\),输出 \(8\).
当 \(res = 2\),输出 \(9\).
当 \(res \ge 3\) ,则后面也不可能凑出超过2个质因数了。
于是做完了
vector<ll> table = {614889782588491410,38655288426304091,22125549654501673,316773187163046517,9879251463499721,39049078408188253,108538288030848139,309619196508457007,796229312542859009,4064625951224869,6860596063872959,10626236358872441,17092564102090369,30150641449095443,43889293834596251,60888412234461547,83850965748659689};
// 0 ~ 16
void solvemain()
{
vector<int> p;
// int cur = 1;
for (auto t : table)
{
cout << "? " << t << endl;
int x;cin >> x;
for (int i = 2;i <= x / i ; i ++)
{
if (x % i == 0)
{
p.pb(i);
x/= i;
}
}
if (x > 1) p.pb(x);
}
const int m = 1e9;
ll ans = 1;
for (int i = 0 ;i < sz(p);i+=2)
{
if (i == sz(p ) - 1)
{
int x = p[i];
while (x <= m / p[i]) x *= p[i];
cout << "? " << x << endl;
cin >> x;
ans = ans *( (int)round(logl(x) / logl(p[i])) + 1);
}
else{
int tp = p[i];while (tp<=m/p[i])tp*=p[i];
int tp2 = p[i+1];while (tp2<=m/p[i+1])tp2*=p[i+1];
cout << "? " << (ll)tp*tp2 << endl;
int x;cin >> x;
int c1 = 0 ,c2 = 0 ;
while (x % p[i] == 0) x /=p[i],c1++;
while (x % p[i + 1] == 0) x /=p[i + 1] ,c2++;
ans = ans * (c1 + 1) * (c2 + 1);
}
}
if (ans == 1)
{
cout <<"! "<< 8 << endl;
return;
}
if (ans == 2)
{
cout <<"! "<< 9 << endl;
return ;
}
cout << "! "<<ans * 2 << endl;
}
CF1385F Removing Leaves
题目描述
给定一棵包含 \(n\) 个顶点的树(无环连通图)。这棵树是无根的——也就是说,它只是一个无环的连通无向图。
每一步,你可以选择恰好 \(k\) 个叶子(叶子是指只与一个其它顶点相连的顶点),这些叶子都与同一个顶点相连,并将它们以及与它们相连的边一起删除。也就是说,你可以选择叶子 \(u_1, u_2, \dots, u_k\),它们都与同一个顶点 \(v\) 相连(存在边 \((u_1, v)\),\((u_2, v)\),\(\dots\),\((u_k, v)\)),然后删除这些叶子和这些边。
你的任务是,若每一步都最优地删除叶子,求最多可以进行多少次这样的操作。
做法
直接按题意模拟任意删都是对的。
证明
假设存在最优的操作序列 opt。
首先我们贪心的操作一定会出现在opt。
假设不出现,我们操作的那几个节点永远不会改变。
因此操作是优的。
考虑证明我们接下来一步贪心的操作在 opt中一定也是第一步。
我们发现先放在第一步完全不劣, 因为它操作了也不会影响任何东西,甚至有帮助。
依次类推,我们归纳证明出了贪心是最优的。
trick:证明一个操作 S 是优的,我们只需要假设存在更优的opt
归纳证明:S的第一个操作一定出现在opt中且可以放在第一个
CF2208D2 Tree Orientation (Hard Version)
你打算随意给每条 \(n-1\) 条边赋予一个方向。你发现了一张记录每一组有序对 \((u,v)\) 满足 \(1\le u,v\le n\),都标记了 \(u\) 是否能够到达 \(v\)。
你想根据这份信息,推断出可能的树的结构以及边的方向。如果有可能,请构造出任意一种解;如果有多个解,只需给出其中一种即可。
做法
\(O(n^3)\) 做法很简单。
枚举 \(x,y\),存在边 \(x \rightarrow y\) 当且仅当不存在 \(x \rightarrow j \rightarrow y\) 之间可达。
考虑优化。
我们枚举的这三个哪个是可以被优化的呢?
事实上是 \(j\),因为我们不需要枚举每一个 \(j\) ,我们只需要判定 \(x\) 的邻居 \(j\) 即可。
这样复杂度就变为了 \(O(n^2)\) 了。
计数好文
https://www.cnblogs.com/gsc0618china/p/19086852
CF1404C Fixed Point Removal
给定一个长度为 \(n\) 的正整数数组 \(a_1, \ldots, a_n\)。每次操作,你可以选择一个下标 \(i\),使得 \(a_i = i\),并将 \(a_i\) 从数组中移除(移除后,剩余部分会自动拼接)。
数组 \(a\) 的“权重”定义为你最多可以移除的元素数量。
你需要回答 \(q\) 个独立的询问 \((x, y)\):将 \(a\) 的前 \(x\) 个元素和后 \(y\) 个元素都替换为 \(n+1\)(使它们无法被移除)后,\(a\) 的权重是多少?
做法
令 \(v_i = i - a_i\)。
每次操作化为选择一个 \(0\),令后面的数 \(-1\),问最多删除多少数。
容易列出 \(dp\):
题目即求区间做一遍 dp 的值。
可以先把 \(f\) 扩展为 \(f_{l,r}\) 表示区间的 dp 值。
对于这种区间求值的问题。
有几种做法:矩阵动态dp,维护dp值端点,扫描线。
我们考虑扫描线。
由于往后添加数比较简单,我们考虑 \(R\) 往后扫。
1
考虑添加 \(a_r\) 怎么维护出 \(\forall i<r,f_{i,r}\)。每个位置 \(i\) 维护的即为 \(f_{i,r}\)。
我们已知 \(\large f_{i,r-1}\)。
则当 \(f_{i,r-1} \ge a_r\) 时,\(f_{i,r} \leftarrow 1\)。
Observation:单调性
这里的性质是:固定的端点,另一端越长答案越大。
于是我们可以二分出需要加的位置,做区间加,可以用树状数组上二分。
int search(int k,int limit)
{
int res = 0 ,p = 0 ;
for (int i = __lg(N);i >= 0;i--)
{
int ne (p + (1<<i));
if (ne <= limit && res + c[ne] >= k)
res += c[ne],p = ne;
}
return p;
}
vector<int> ans(q + 2);
rep(r,1,n)
{
int pos = search(a[r] , r - 1) ;
add(1,pos,1),add(r,r,(a[r]==0));
for (auto [l , id] : Qr[r])ans[id] = query(l);
}
rep(i,1,q) cout << ans[i] << endl;
- 这个 DP 值关于初始状态一定具有单调性。
- 特征: 题目给了很多区间询问,而且不带修改。
2
其实用 \(L\) 从右往左扫也是可做的。因为有观察:右端点不会影响点能不能删,只起到一个划定范围的作用。
假设增加 \(a_l=0\)。
则会有一个 \([l + 1,n] \leftarrow 1\) 的贡献。
但是会有连锁反应很烦。
trick:连锁反应?!
事实上不烦。
因为我们只需要维护 \(c_i\) 表示 \(i\) 现在能不能被删。
在扫描线的过程中,\(c_i\) 只会被改一次。
而我们只需要线段树(维护区间最小值及其位置,支持区间加减)和一个树状数组(BIT)(维护已经删除的元素个数,用于回答询问)来模拟这个过程即可。
P14568 【MX-S12-T3】排列
trick:排列计数插入法?!
求出有多少个 \(1 \sim n\) 的排列 \(a_1, \ldots, a_n\) 满足以下条件:
对于每个下标 \(i\)(\(1 \le i \le n\)),
- \(a_i < \min_{j < i} a_j\) 或
- \(a_i > \max_{j < i} a_j\) 或
- \(a_i < \min_{j > i} a_j\) 或
- \(a_i > \max_{j > i} a_j\)。
对每个下标 \(i\),其满足四种情况中的哪一种由输入给出。答案对 \(998244353\) 取模。
对于排列计数,可以考虑按顺序插入,只考虑相对位置。
具体地,考虑将 \(a_i\) 插入前面构成的序列的哪里,有 \(i\) 个插入点。
再根据题目的限制 dp。
当我们要把某个数插入到位置 \(i\) 的时候:
-
\(op_i=0\) 时,要求其左侧最终没有比它更小的数。即在插入按递增的过程中,这只能在当前它已剥余区间的最左端时满足。
-
\(op_i=1\) 时,同理只能在剩余区间最右端才能满足。
-
\(op_i=2\) 时,要求其左侧最终的最大值小于它。递增插入时,这等价于当前已放的左侧不存在。
-
\(op_i=3\) 时,同 \(op_i=2\)。
所以我们就可以列出来一个状态
表示当前已经插入了前 \(i\) 个位置,并且还剩余 \(j\) 个位置可插。
转移是容易的:
- 当 \(op\in\{0,1\}\) 时,每次插入相当于新增一个可插入位置。直接由
转移过来即可。
- 当 \(op\in\{2,3\}\) 时,放这个数时它要放在已有的已预留的位置中的某一个,放完后可插入位置数不会增加,而且 \(j\) 可以变成任意不超过原来的值,因此由
转移而来。

浙公网安备 33010602011771号