10月15日模拟赛题解
10月15日模拟赛题解
A 树
Description
给定一棵 \(n\) 个节点的树,每个节点有两个参数 \(a,~b\),对于每个节点,求子树中参数为 \(b\) 的所有节点的 \(a\) 之和
Limitations
\(100\%\) \(1 \leq b \leq n \leq 10^5,~a \leq 1000\)
\(60\%\) \(1 \leq b,n\leq 1000\)
\(30\%\) \(1 \leq b, n \leq 10\)
Solution
对于 \(30\%\) 的数据,我也不会做。
对于 \(60\%\) 的数据,考虑枚举一下 \(b\),然后 \(O(n)\) 在树上 dfs 即可。
考虑满分做法。
一个显而易见的想法是,对于一个 \(b_i\),我们遍历他子树中参数 \(b\) 与 \(b_i\) 不同的节点是没有意义的,因此我们考虑遍历完一个节点以后直接跳到它子树内最靠近它的 \(b\) 相同的节点,换句话说,对每个节点,寻找一个与它参数 \(b\) 相同的最近的祖先作为父节点。
考虑在 dfs
的时候,如果我们对每个 \(b\) 维护根到当且节点的链上参数为 \(b\) 的节点序列,则当前节点的同参数父节点就是对应序列的最后一个节点。考虑维护这个节点序列只需要开 \(n\) 个栈分别维护每个 \(b\) 的序列即可,即 dfs
到一个节点时将节点入栈,离开时弹出,这样显然可以保证栈内元素是有序的根到当前节点链上的节点序列。
然后我们相当于对于每个 \(b\) 都新建了一棵树,在这些树上做 dfs
即可。事实上在原来的树上做 dfs
的时候只要维护好每个节点在新树上的孩子就可以直接统计,不需要显式地建出新树。
时空复杂度 \(O(n)\),期望得分 \(100~pts\)
Code
#include <cstdio>
#include <stack>
#include <vector>
const int maxn = 100010;
struct Edge {
int v;
Edge *nxt;
Edge(const int _v, Edge *h) : v(_v), nxt(h) {}
};
Edge *hd[maxn];
int n;
int b[maxn], ans[maxn];
bool vis[maxn];
std::stack<int>stk[maxn];
std::vector<int>son[maxn];
void dfs(const int u);
int main() {
freopen("tree.in", "r", stdin);
freopen("tree.out", "w", stdout);
qr(n);
for (int i = 1; i <= n; ++i) {
qr(ans[i]); qr(b[i]);
}
for (int i = 1, u, v; i < n; ++i) {
u = v = 0; qr(u); qr(v);
hd[u] = new Edge(v, hd[u]);
hd[v] = new Edge(u, hd[v]);
}
dfs(1);
for (int i = 1; i <= n; ++i) {
qw(ans[i], i == n ? '\n' : ' ', true);
}
return 0;
}
void dfs(const int u) {
vis[u] = true;
int k = b[u];
if (stk[k].size()) {
son[stk[k].top()].push_back(u);
}
stk[k].push(u);
for (auto e = hd[u]; e; e = e->nxt) if (!vis[e->v]) {
int v = e->v;
dfs(v);
}
for (auto v : son[u]) {
ans[u] += ans[v];
}
stk[k].pop();
}
B 小偷
Description
有一个长度不低于 \(4\) 的序列 \(A\),满足 \(\frac{A_i}{A_{i - 1}} = k\),其中 \(k\) 是一个正整常数,且序列的最大值不超过 \(n\)。
显然这样的合法序列有许多个,现在我们对序列的参数一无所知,只知道合法的序列有 \(m\) 个,求 \(n\) 最小是多少。无解输出 -1
。
Limitations
\(100\%\) \(3 \leq m \leq 10^{15}\)
\(60\%\) \(3 \leq m \leq 100000\)
\(30\%\) \(3 \leq m \leq 1000\)
Solution
对于前 \(30\%\) 的数据,爆搜一下序列即可。
对于前 \(60\%\) 的数据,枚举一下 \(k\) 即可,然后枚举序列长度即可。值得注意的是由于序列每次除以 \(k\),因此序列长度是 \(O(\log)\) 级别的,并且序列最大值大概是 \(O(m)\) 级别,最大值至少除 \(3\) 次 \(k\) 后的结果必须是正数,因此 \(k\) 也不会很大,大概在 \(10^2\) 数量级,因此枚举可过。时间复杂度 \(O(m \log m)\)
至于证明最大值是 \(O(m)\) 的……我是打个表看出来的(雾
对于全部的数据,考虑求最小值,并且显然不可能贪心,DP由于序列长度不确定也无法设计状态,因此考虑二分答案。
考虑固定一个 \(n\),当 \(n\) 变大时,合法的序列个数不会变小,因为 \(n\) 较小时合法的序列,当 \(n\) 变大后依然是合法的。可以据此二分答案。
二分答案后,考虑枚举首相 \(k\),然后枚举序列长度即可。同样由于 \(n\) 是 \(O(m)\) 的,得到 \(k \leq 10^\frac{15}{4} \leq 10^4\)。又因为序列长度是 \(O(\log m)\) 的,因此求方案数的总复杂度是 \(O(\log m 10^\frac{\log m}{4})\)。
因此总的时间复杂度是 \(O(10^{\frac{\log m}{4}} \log^2 m)\)。
#include <cstdio>
typedef long long int ll;
ll m, ans;
ll calc(const ll n);
int main() {
freopen("thief.in", "r", stdin);
freopen("thief.out", "w", stdout);
qr(m);
for (ll l = 1, r = 1000000000000000000ll, mid = 1000000000ll; l <= r; mid = (l + r) >> 1) {
if (calc(mid) >= m) {
ans = mid; r = mid - 1;
} else {
l = mid + 1;
}
}
qw(calc(ans) == m ? ans : -1, '\n', true);
return 0;
}
ll calc(const ll n) {
ll _ret = 0;
for (int k = 2; ; ++k) {
ll cnt = 0;
for (ll i = n; i; i /= k) {
if ((++cnt) >= 4) { _ret += i; }
}
if (cnt < 4) break;
}
return _ret;
}
C 盘子
Description
给定 \(n\) 个圆,第 \(i\) 个圆的半径为 \(r_i\),又有 \(m\) 个间距为 \(1\) 且排成一排的点,要求以这些点作为这 \(n\) 个圆的圆心,且圆之间两两不相交但允许相切,求方案数对大质数取模的结果。
Limitations
\(100\%\) \(3 \leq n \leq 4000,~m \leq 10^9, r_i \leq 10^5\)
\(60\%\) \(3 \leq n \leq 100,~~m \leq 200\)
\(30\%\) \(2 \leq n \leq 5,~~m \leq 30\)
Solution
前 \(30\%\) 爆搜即可。时间复杂度 \(C_{m}^{n}\)。
对于 \(60\%\) 的数据,考虑只考虑圆心所在的直线,则问题转化为了给定一堆线段,求互不相交的安放方案数。
考虑把一段长度为 \(1\) 的空白也看做一个元素,则问题转化为了有重复元素的全排列数,套公式即可。
注意到最两边的线段是可以有至多一半露在外面的,考虑枚举这两端的元素,然后枚举他们有多少露在外面,暴力算每种情况的方案数公式,注意到露在外面的每向里 \(1\) 的长度时,这种情况的方案数可以 \(O(1)\) 维护,这样就得到了一个 \(O(n^3 + n^2 r)\) 的做法,然后注意到两侧线段向里移动的过程中,每个数字在公式中产生的贡献是可以 \(O(1)\) 计算的,因此得到了一个 \(O(n^3 + n^2 \times poly(\log))\) 的做法,但是没调出来。