2020牛客暑期多校训练营(第七场)
| 题号 | A | B | C | D | E | F | G | H | I | J |
|---|---|---|---|---|---|---|---|---|---|---|
| 赛中 | 🎈 | 🎈 | 🎈 | 💭 | 💭 | |||||
| 赛后 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
A - Social Distancing
题意
思路
代码
B - Mask Allocation
题意
将\(n\times m\)个口罩打包成若干份,要求打包完后无论是分给\(n\)家医院还是\(m\)家医院,都能够均分。要求输出总份数最少的方案中字典序最大的。(\(1\le T\le 100, 1\le n,m\le 10^4\))
思路
假设\(n>m\),分给\(m\)家医院的最佳方案是\(n, n,\cdots,n(m个n)\),分给\(n\)家医院的最佳方案是\(m, m,\cdots,m(n个m)\),则答案至少有\(n\)份(即分给\(n\)家医院的最佳方案)。考虑在这个方案上进行调整,保留前\(m\)个\(m\)分给\(m\)家医院(这样可以使得字典序最大),则问题从\(\text{solve}(n, m)\)变为了\(\text{solve}(n-m,m)\),故可递归求解,递归边界为\([n=m]\)。
时间复杂度\(O(\max(n,m))\)。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
const int maxn = 1e4 + 10;
int n, m;
vector<int> ans;
void DFS(int n, int m)
{
if(n < m)
swap(n, m);
for(int i = 1; i <= m; i++)
ans.push_back(m);
if(n != m)
DFS(n - m, m);
}
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
scanf("%d %d", &n, &m);
ans.clear();
DFS(n, m);
printf("%d\n", ans.size());
for(int i = 0; i < ans.size(); i++)
{
if(i > 0)
printf(" ");
printf("%d", ans[i]);
}
printf("\n");
}
return 0;
}
C - A National Pandemic
题意
题目大意:一棵无根树,每个点都有一个点权 f(x),其初值均为 0,有三种操作。
操作1:对所有的 y,修改f(y) 为 w - dist(x,y)
操作2:修改 f(x) 为 min(f(x),0)
操作3:查询 f(x)
思路
无根树转有根树做法:
\(w - dist(x,y) = w - dep[x] - dep[y] + 2 * dep[lca]\) ,\(w - dep[x] - dep[y]\) 可以直接维护,树剖修改 \(x\) 到根路径上的点,令 cnt += 2,则 \(2 * dep[lca]\) 等于到根路径上的点权和,复杂度为 \(n\log^2n\),常数较小
分析:
\(w - dist(x,y) = w - dep[x] - dep[y] + 2 * dep[lca]\)
对于 \(w - dep[x] - dep[y]\) 可以直接维护得到,关键是统计 \(2 * dep[lca]\),查询点 u 时,暴力爬链,枚举所有 u 的父亲,设 u 的父亲按深度从大到小排序依次为 \(x_1,x_2,...,x_n\) ,则 u 点 \(2 * dep[lca]\) 部分的贡献为:\(2*cnt[x_1] *dep[x_1] + 2 * (cnt[x_2]- cnt[x_1])*dep[x_2] + 2 * (cnt[x_3] - cnt[x_2]) * dep[x_3]+...+2*(cnt[x_n] - cnt[x_{n-1}])*dep[x_n]\)
其中,\(cnt[u]\) 表示 \(u\) 的子树中,修改点的数量,上述贡献式子化简一下就得到 \(2 * cnt[x_1] + 2 * cnt[x_2] + ... + 2 * cnt[x_n] * dep[x_n]\),而 \(x_n\) 一定为根节点,\(dep[x_n]\) 为 1,因此每次修改 \(x\) 时,将 \(x\) 到根节点路径上的所有点的 \(cnt += 2\),查询时该点的答案就是到根节点路径上的点权和
点分树做法:
构建点分树,对每个点维护子树内所有修改点 \(x\) 到其的距离之和,查询时暴力爬链合并得到所有的 \(w - dist(x,y)\),复杂度为 \(n\log n\),常数小,跑的快
https://blog.csdn.net/qq_41997978/article/details/107742275
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 100;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int n, m, RT, t;
inline int read()
{
int x=0,f=1;char ch;
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Graph {
int head[maxn], to[maxn << 1], cnt, nxt[maxn << 1], w[maxn << 1];
void init() {
for (int i = 1; i <= n; i++)
head[i] = -1;
cnt = 0;
}
void add(int u,int v,int c) {
to[cnt] = v;
nxt[cnt] = head[u];
w[cnt] = c;
head[u] = cnt++;
to[cnt] = u;
nxt[cnt] = head[v];
w[cnt] = c;
head[v] = cnt++;
}
} G;
struct divide_tree {
int dep[maxn],vis[maxn],f[maxn],root,sz[maxn],siz,p[maxn][20],dis[maxn][20]; //预处理树的重心等
ll val[maxn], fval[maxn], optcnt[maxn], sum[maxn]; //维护答案等, val 为子树内的贡献,fval[maxn]为 子树到父节点的贡献 , sum 维护 delta
void getroot(int u,int fa) { //求一棵子树的重心
sz[u] = 1; f[u] = 0;
for (int i = G.head[u]; i + 1; i = G.nxt[i]) {
int v = G.to[i];
if (v == fa || vis[v]) continue;
getroot(v,u);
sz[u] += sz[v];
if (sz[v] > f[u]) f[u] = sz[v];
}
if (siz - sz[u] > f[u]) f[u] = siz - sz[u];
if (!root || f[u] < f[root]) root = u;
}
void getship(int u,int anc,int father,int d) {
for (int i = G.head[u]; i + 1; i = G.nxt[i]) {
int v = G.to[i];
if (!vis[v] && v != father) {
++dep[v];
p[v][dep[v]] = anc;
dis[v][dep[v]] = d;
getship(v,anc,u,d + 1);
}
}
}
void Buildtree(int u) { //点分构建点分树
vis[u] = 1; getship(u,u,0,1);
int all = siz;
for (int i = G.head[u]; i + 1; i = G.nxt[i]) {
int v = G.to[i];
if (vis[v]) continue;
root = 0, siz = sz[v];
if (siz > sz[u]) siz = all - sz[u];
getroot(v,u); Buildtree(root);
}
}
void prework() {
for (int i = 1; i <= n; i++)
val[i] = fval[i] = optcnt[i] = sum[i] = dep[i] = vis[i] = 0;
siz = n; f[0] = inf; root = 0;
getroot(1,0);
Buildtree(root);
for (int i = 1; i <= n; i++)
p[i][dep[i] + 1] = i;
}
void update(int x,int w) {
val[x] += w; optcnt[x] += 1;
for (int i = dep[x]; i; i--) {
val[p[x][i]] += w - dis[x][i];
fval[p[x][i + 1]] += w - dis[x][i];
optcnt[p[x][i]] += 1;
}
}
ll qry(int u) {
ll ans = 0;
ans = val[u]; int tmp = u;
for (int i = dep[u]; i; i--)
ans = ans + (val[p[u][i]] - fval[p[u][i + 1]]) - 1ll * dis[u][i] * (optcnt[p[u][i]] - optcnt[p[u][i + 1]]);
return ans + sum[u];
}
}tree;
int main() {
t = read();
while (t--) {
n = read(); m = read();
G.init();
for (int i = 1; i < n; i++) {
int u,v,w; u = read(); v = read();
G.add(u,v,1);
}
tree.prework();
while (m--) {
int op, x, w; op = read();
if (op == 1) {
x = read(); w = read();
tree.update(x,w);
} else if (op == 2) {
x = read();
ll tmp = tree.qry(x);
if (tmp > 0)
tree.sum[x] -= tmp;
} else if (op == 3) {
x = read();
printf("%lld\n",tree.qry(x));
}
}
}
return 0;
}
D - Fake News
题意
判断\(\sum_{k=1}^n k^2\)是否为完全平方数(\(1\le T\le 10^6, 1\le n\le 10^{15}\))
思路
打表到\(10^6\)发现只有\(1\)和\(24\),蒙了一发过了。实际可证明。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
const int maxn = 1e5 + 10;
LL n;
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
scanf("%lld", &n);
if(n == 1 || n == 24)
printf("Fake news!\n");
else
printf("Nobody knows it better than me!\n");
}
return 0;
}
E - NeoMole Synthesis
题意
思路
代码
F - Tokens on the Tree
题意
思路
代码
G - Topo Counting
题意
思路
代码
H - Dividing
题意
\((1,k)\) 是 legend tuple,\((xk,k)\) 是 legend tuple,\((1 + xk,k)\) 是legend tuple,给定 \(n,K\) 问有多少 \((x,y)\) 满足 \(x \leq n, y \leq K\) 且 (x,y) 是 legend tuple
思路
枚举 \(k\),\((xk,k)\) 的贡献为 \(\lfloor\frac{n}{k}\rfloor\),\((1 + xk,k)\) 的贡献为 \(\lfloor\frac{n - 1}{k}\rfloor + 1\),处理一下 k 为 1的情况,\(k > 1\) 时分下块
复杂度为 \(\sqrt{(min(n,K))}\)
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
ll n, k, inv2 = 500000004;
int main() {
scanf("%lld%lld",&n,&k);
ll ans = 0, res = 0;
for (ll i = 2, j; i <= min(n,k); i = j + 1) {
j = min(n / (n / i),k);
ans = (ans + (n / i) % mod * (j - i + 1) % mod) % mod;
}
n--;
for (ll i = 1, j; i <= min(n,k); i = j + 1) {
j = min(n / (n / i),k);
ans = (ans + (n / i) % mod * (j - i + 1) % mod) % mod;
}
printf("%lld\n",(ans + k) % mod);
return 0;
}
I - Valuable Forests
题意
定义所有结点的度数平方和为森林的权值,求含有\(N\)个带标记结点的所有森林的权值和。(\(1\le T\le 5000, 1\le N\le 5000\))
思路
第一次知道这个无根树计数的神器 —— prufer序列,主要利用其以下两条性质:
- 一棵\(n\)个结点的无根树唯一地对应了一个长度为\(n-2\)的prufer序列,序列中的每个数都在\(1\)到\(n\)的范围内;
- prufer序列中某个编号出现的次数就等于这个编号的结点在无根树中的度数\(-1\)。
根据性质1,可知\(n\)个结点的无根树的数量即为:
这题的计数还是挺巧妙的,首先预处理出\(n\)个结点的森林个数:
即从原有的\(n-1\)个结点中选出\(i\)个与第\(n\)个结点组成一棵无根树。
结合性质2,再处理出一棵\(n\)个结点的无根树的贡献:
枚举一个结点的度数并计算贡献,由于所有点贡献相同,再乘以\(n\)即可。
最后计算答案,\(n\)个结点的森林的权值和:
即从原有的\(n-1\)个结点中选出\(i\)个与第\(n\)个结点组成一棵无根树,会产生两部分贡献(如上式),前者是构成的无根树产生的贡献(要乘以其他点构成的森林数量),后者是其他点构成的森林产生的贡献(要乘以构成的无根树的数量)。
预处理时间复杂度\(O(N^2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
const int maxn = 5000 + 10;
int MOD, N;
LL comb[maxn][maxn], power[maxn][maxn];
LL calc(LL n)
{
if(n < 2)
return 1;
else
return power[n][n - 2];
}
LL f[maxn], g[maxn], h[maxn];
void preprocess()
{
comb[0][0] = 1;
for(int i = 1; i <= 5000; i++)
{
comb[i][0] = 1;
for(int j = 1; j <= i; j++)
comb[i][j] = (comb[i - 1][j] + comb[i - 1][j - 1]) % MOD;
}
for(int i = 0; i <= 5000; i++)
{
power[i][0] = 1;
for(int j = 1; j <= 5000; j++)
power[i][j] = power[i][j - 1] * i % MOD;
}
f[0] = 1;
for(int i = 1; i <= 5000; i++)
for(int j = 0; j <= i - 1; j++)
f[i] = (f[i] + comb[i - 1][j] * calc(j + 1) % MOD * f[i - 1 - j] % MOD) % MOD;
for(int i = 1; i <= 5000; i++)
{
for(int d = 1; d <= i - 1; d++)
g[i] = (g[i] + comb[i - 2][d - 1] * power[i - 1][i - 1 - d] % MOD * d % MOD * d % MOD) % MOD;
g[i] = g[i] * i % MOD;
}
for(int i = 1; i <= 5000; i++)
for(int j = 0; j <= i - 1; j++)
h[i] = (h[i] + comb[i - 1][j] * ((f[i - 1 - j] * g[j + 1] % MOD + calc(j + 1) * h[i - 1 - j] % MOD) % MOD) % MOD) % MOD;
}
int main()
{
int T;
scanf("%d %d", &T, &MOD);
preprocess();
while(T--)
{
scanf("%d", &N);
printf("%lld\n", h[N]);
}
return 0;
}
J - Pointer Analysis
题意
\(26\)个小写字母表示的对象,每个对象都有\(26\)个小写字母表示的成员变量,该成员变量是指针;\(26\)个大写字母表示的全局指针变量。
有\(n\)个\(4\)种类型的赋值语句,在执行时这\(n\)个语句的执行顺序可以任意打乱,问每个全局指针可以指向哪些对象。
思路
考虑到\(n\)最大只有\(200\),比较小,可以让这\(n\)条语句重复\(n\)次,这样就可以保证每条语句在最后一次执行之前,其他的所有语句都已经执行过至少一次,实现语句乱序执行的效果。
\(p1[i][j]\)标记第全局指针\(i+'A'\)是否可以指向对象\(j+'a'\),\(p2[i][j][k]\)表示对象\(i+'a'\)的成员指针\(j+'a'\)是否指向对象\(k+'a'\),在遍历所有语句的时候判断语句类型,根据题意更新两个数组。注意只有当全局指针指向对象之后才会有\(B.f\)这种形式(因为本题中只有对象有成员指针),\(getline\)读入整行的时候不要忘记加速\(cin\)的读入。
时间复杂度\(O(n^2{26}^2)\),空间复杂度\(O(26^3)\)
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4e5;
int n;
string s[maxn];
string t;
bool p1[26][26], p2[26][26][26];
int main()
{
//freopen("/Users/zhangkanqi/Desktop/11.txt","r",stdin);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin>>n;
getline(cin, t);
for(int i=1; i<=n; ++i) getline(cin, s[i]);
for(int t=1; t<=n; ++t) //前面的n条表达式重复n次
for(int i=1; i<=n; ++i) s[t*n+i]=s[i];
n=n*n;
for(int i=1; i<=n; ++i)
{
if(s[i].size()==5&&islower(s[i][4]))
{
int a=s[i][0]-'A';
int x=s[i][4]-'a';
p1[a][x] = true;
}
else if(s[i].size()==5&&isupper(s[i][4]))
{
int a=s[i][0]-'A';
int b=s[i][4]-'A';
for(int j=0; j<26; ++j) if(p1[b][j]) p1[a][j] = p1[b][j];
}
else if(s[i].size()==7&&s[i][1]=='.')
{
int a=s[i][0]-'A', f=s[i][2]-'a';
int b=s[i][6]-'A';
for(int j=0; j<26; ++j)
if(p1[a][j])
for(int k=0; k<26; ++k) if(p1[b][k]) p2[j][f][k] = p1[b][k];
}
else if(s[i].size()==7&&s[i][5]=='.')
{
int a=s[i][0]-'A';
int b=s[i][4]-'A', f=s[i][6]-'a';
for(int j=0; j<26; ++j)
if(p1[b][j])
for(int k=0; k<26; ++k) if(p2[j][f][k]) p1[a][k] = p2[j][f][k];
}
}
for(int i=0; i<26; ++i)
{
cout << char('A'+i) << ": ";
for(int j=0; j<26; ++j)
if(p1[i][j]) cout << char('a'+j);
cout << endl;
}
}

浙公网安备 33010602011771号