2020牛客寒假算法基础集训营1
题目链接:https://ac.nowcoder.com/acm/contest/3002
题解链接:https://ac.nowcoder.com/discuss/364600
B - kotori和bangdream
水题。
D - hanayo和米饭
水题。
E - rin和快速迭代
题意:设 \(f(x)\) 为 \(x\) 的因数的个数,猜想每次用 \(x:=f(x)\) 迭代下去,所有正整数最后都会变成 \(2\) 。每次输入一个 \(n(3\leq n \leq 10^{12})\) ,求迭代到 \(2\) 需要几次?
题解:首先 \(f(1)=1\) 猜想不成立……不过题目输入的 \(n(3\leq n \leq 10^{12})\) ,显然当 \(n\) 为奇质数的时候只需要1次,而假如 \(n\) 是合数则会变小为至多 \(O(\sqrt{n})\) ,单次分解是 \(O(\sqrt{n})\) 的,这个求和貌似是 \(O(n^{\frac{3}{4}})\) ,而且因数的个数实际上远小于 \(O(\sqrt{n})\) 。
有一种复杂度完全正确的做法,用线性筛筛出 \(2\cdot 10^6\) 以内每个数的因数的个数。同时用dp方程 \(dp[f(x)]=dp[x]+1\) 求出每个数需要的迭代次数。最后对 \(n\) 进行一次分解即可。胆子更大的话可以把更大的 \(n\) 用 Pollard_rho 分解。
int fac(ll x) {
int cnt = 0;
for(ll i = 1; i * i <= x; ++i) {
if(x % i == 0) {
++cnt;
if(i * i != x)
++cnt;
}
}
return cnt;
}
void test_case() {
ll n;
scanf("%lld", &n);
int res = fac(n);
int cnt = 1;
while(res != 2) {
n = res;
res = fac(n);
++cnt;
}
printf("%d\n", cnt);
}
G - eli和字符串
水题。
*A - honoka和格点三角形
吐槽:这个有点复杂的计数题居然过得这么多,看来我国icpc的计数水平比我高很多。
题意:取 \(n*m\) 个整数点构成矩阵,用其中的三个点构成三角形。并要求:
1、三角形的面积为1;
2、三角形至少有一边与x轴或y轴平行;
吐槽:一开始看成“三角形有两边与x轴或y轴平行”,还好过不了样例,然后搞了好几个假算法演了几次。
题解:首先把三角形与x轴或y轴平行的那条边称为“底”,那么剩下的那个点到“底”的距离就是“高”,由于都是整点所以底只能是1或2,对应的高必须是2或1。
选择这样的三角形分几步:
1、确定“底”平行于那条轴,以及“底”的长度
2、确定“高”的指向,是指从“底”指向剩下的那个点的方向,这个会对“底”能选的直线产生限制。
3、确定“底”所在的直线
4、确定“底”在直线上具体的位置
5、确定“高”的位置,也就是剩下的那个点的位置
以上各步独立,用分步乘法计数原理。
最后因为有的三角形既和x轴平行又和y轴平行,会被计数两次,这样的三角形是直角边分别为1和2的直角三角形,把它们去掉。
void test_case() {
ll n, m;
scanf("%lld%lld", &n, &m);
ll res1 = ((n - 1) * (m - 2)) % MOD * m % MOD * 2 % MOD;
ll res2 = ((m - 1) * (n - 2)) % MOD * n % MOD * 2 % MOD;
ll res3 = ((n - 2) * (m - 1)) % MOD * m % MOD * 2 % MOD;
ll res4 = ((m - 2) * (n - 1)) % MOD * n % MOD * 2 % MOD;
ll res5 = ((n - 1) * (m - 2)) % MOD * 4 % MOD;
ll res6 = ((m - 1) * (n - 2)) % MOD * 4 % MOD;
printf("%lld\n", ((res1 + res2 + res3 + res4 - res5 - res6) % MOD + MOD) % MOD);
}
H - nozomi和字符串
水题,但是怎么比A过得还少,可能我科技树点歪了。
题意:有一个01串,每次操作可以翻转一个位置,进行至多 \(k\) 次操作,求能构成的最长的一段连续相同的子串。
题解:这个显然可以二分,只需要想怎么check。枚举长度len之后,可以尺取转移出当前段内的0的数量和1的数量,只需要其中一种<=k就可以直接全部翻转。
*I - nico和niconiconi
题意:给一个字符串,其中“nico”可以获得得分 \(a\) ,“niconi”可以获得得分 \(b\) ,“niconiconi”可以获得得分 \(c\) ,注意不能够重复获得得分!例如“niconico”要么当作“nico”+“nico”计 \(2a\) 分,要么当作“niconi”+“co”计 \(c\) 分。
题解:看起来就很像匹配字符串的一个自动机?假如知道当前匹配的长度,那么能够去往的下一个状态必定是唯一的。但是正确设出状态需要动一下脑子。
设 \(dp[i][j]\) 为前 \(i\) 个字符当前的末尾有 \(j\) 个字符未获得得分,由于题目的三个串依次是前缀,所以一共有 \([0,10]\) 这么多种匹配长度。
规定1:只有当某一步把匹配长度恰好为4,6和10的状态消去才能获得对应的得分!
那么怎么解决一个字符串同时匹配多种得分组合的情况呢?只需要
规定2:任何时候都可以主动截断现在的已匹配长度,获得对应的得分!
这样在完成一次匹配的时候会自动更新对应的状态。
最后,小心在传参的时候溢出,而且为了更快实现规定2所需的操作,维护一个前一个字符各个状态的最大值就可以了。
int n, a, b, c;
char s[300005];
ll dp[300005][11];
ll max3(ll a, ll b, ll c) {
return max(a, max(b, c));
}
ll max4(ll a, ll b, ll c, ll d) {
return max(max(a, b), max(c, d));
}
void test_case() {
scanf("%d%d%d%d%s", &n, &a, &b, &c, s + 1);
ll premax = 0;
for(int j = 1; j <= 10; ++j)
dp[0][j] = -LINF;
for(int i = 1; i <= n; ++i) {
if(s[i] == 'n') {
dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
dp[i][1] = dp[i][0];
dp[i][2] = -LINF;
dp[i][3] = -LINF;
dp[i][4] = -LINF;
dp[i][5] = max(dp[i - 1][4], dp[i - 1][8]);
dp[i][6] = -LINF;
dp[i][7] = -LINF;
dp[i][8] = -LINF;
dp[i][9] = dp[i - 1][8];
dp[i][10] = -LINF;
} else if(s[i] == 'i') {
dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
dp[i][1] = -LINF;
dp[i][2] = max3(dp[i - 1][1], dp[i - 1][5], dp[i - 1][9]);
dp[i][3] = -LINF;
dp[i][4] = -LINF;
dp[i][5] = -LINF;
dp[i][6] = max(dp[i - 1][5], dp[i - 1][9]);
dp[i][7] = -LINF;
dp[i][8] = -LINF;
dp[i][9] = -LINF;
dp[i][10] = dp[i - 1][9];
} else if(s[i] == 'c') {
dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
dp[i][1] = -LINF;
dp[i][2] = -LINF;
dp[i][3] = max3(dp[i - 1][2], dp[i - 1][6], dp[i - 1][10]);
dp[i][4] = -LINF;
dp[i][5] = -LINF;
dp[i][6] = -LINF;
dp[i][7] = max(dp[i - 1][6], dp[i - 1][10]);
dp[i][8] = -LINF;
dp[i][9] = -LINF;
dp[i][10] = -LINF;
} else if(s[i] == 'o') {
dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
dp[i][1] = -LINF;
dp[i][2] = -LINF;
dp[i][3] = -LINF;
dp[i][4] = max(dp[i - 1][3], dp[i - 1][7]);
dp[i][5] = -LINF;
dp[i][6] = -LINF;
dp[i][7] = -LINF;
dp[i][8] = dp[i - 1][7];
dp[i][9] = -LINF;
dp[i][10] = -LINF;
} else {
dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
dp[i][1] = -LINF;
dp[i][2] = -LINF;
dp[i][3] = -LINF;
dp[i][4] = -LINF;
dp[i][5] = -LINF;
dp[i][6] = -LINF;
dp[i][7] = -LINF;
dp[i][8] = -LINF;
dp[i][9] = -LINF;
dp[i][10] = -LINF;
}
premax = -INF;
for(int j = 0; j <= 10; ++j) {
//printf("dp[%d][%d]=%lld\n", i, j, dp[i][j]);
premax = max(premax, dp[i][j]);
}
//puts("");
}
ll ans = max4(premax, dp[n][4] + a, dp[n][6] + b, dp[n][10] + c);
printf("%lld\n", ans);
}
其中应该有些转移是多余的,但是无所谓。
*F - maki和tree
挺有意思的一个简单树形dp。启示来源于树分治的例题中把路径分成几类的的方法。
题意:给一棵树,每个点或黑或白,求有多少条长度>=2的路径,满足恰好只经过一个黑点。这里路径的长度定义为经过的点的数量。
题解:首先特判掉n<=2的奇异情况,以后每次做树的题我都先特判掉奇异情况。然后这样的树就必定有至少一个非叶子的点,选第一个这样的点作为根,这样根就不会是叶子了(虽然在这道题并不影响,不过可能是昨晚一道树形题的后遗症)。
然后定好根之后,每条路径必定满足下面两种之一(规定u的深度不深于v的深度):
1、路径(u,v),其中lca(u,v)=u,即从u向v向下走的路。
2、路径(u,v),其中lca(u,v)!=u,可以分解为从lca向u和向v向下走的两条路。
也就是type2的类型可以分解为两条向下路径的组合。需要维护的就是从当前的根向下走的,没有经过任何黑点的路 \(cnt0\) ,以及恰好只经过一个黑点的路 \(cnt1\) 。
很明显这个东西假如从叶子开始合并子树的话,每次只受新加入的根的影响。具体为:
若根u是黑点,那么
\(cnt0[u]=0\)
\(cnt1[u]=1+\sum\limits_{v \in son[u]} cnt0[v]\)
若根u是白点,那么
\(cnt0[u]=1+\sum\limits_{v \in son[u]} cnt0[v]\)
\(cnt1[u]=\sum\limits_{v \in son[u]} cnt1[v]\)
维护的方法已经有了,那么按照上面的路径种类分别计数,type1的直接由 \(cnt1[u]\) 的定义得就是这个东西,不过要减掉终点也是根的那个1。
type2的,若根u是黑点,那么应该选两条没有经过任何黑点的路,且这两条路走向不同的子树。若根u是白点,那么应该选一条没有经过任何黑点的路,和一条恰好只经过一个黑点的路,且这两条路走向不同的子树。
char s[100005];
int color[100005];
vector<int> G[100005];
int cnt0[100005], cnt1[100005];
ll sum;
void dfs(int u, int p) {
if(G[u].size() == 1) {
if(color[u]) {
cnt0[u] = 0;
cnt1[u] = 1;
} else {
cnt0[u] = 1;
cnt1[u] = 0;
}
return;
}
int sumcnt0 = 0;
int sumcnt1 = 0;
for(auto &v : G[u]) {
if(v == p)
continue;
dfs(v, u);
sumcnt0 += cnt0[v];
sumcnt1 += cnt1[v];
}
if(color[u]) {
sum += sumcnt0;
ll tmp = 0;
for(auto &v : G[u]) {
if(v == p)
continue;
tmp += 1ll * (sumcnt0 - cnt0[v]) * (cnt0[v]);
}
sum += tmp / 2;
cnt0[u] = 0;
cnt1[u] = 1 + sumcnt0;
return;
} else {
sum += sumcnt1;
ll tmp = 0;
for(auto &v : G[u]) {
if(v == p)
continue;
tmp += 1ll * (sumcnt1 - cnt1[v]) * (cnt0[v]);
}
sum += tmp;
cnt0[u] = 1 + sumcnt0;
cnt1[u] = sumcnt1;
return;
}
}
void test_case() {
int n;
scanf("%d%s", &n, s + 1);
for(int i = 1; i <= n; ++i) {
if(s[i] == 'B')
color[i] = 1;
}
for(int i = 1; i <= n - 1; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
if(n == 1) {
puts("0");
return;
}
if(n == 2) {
if(color[1] + color[2] == 1) {
puts("1");
return;
}
puts("0");
return;
}
int r = -1;
for(int i = 1; i <= n; ++i) {
if(G[i].size() != 1) {
r = i;
break;
}
}
sum = 0;
dfs(r, -1);
printf("%lld\n", sum);
}
反思:看了题解原来是数每个白点连通块,然后相邻两个连通块之间乘法选就可以了。不需要搞这么复杂233。
J - u's的影响力
可以把递推式写出来,发现指数满足某种线性递推的dp规律,所以可以用矩阵快速幂或者BM来解出正确的指数,然后套个快速幂就有答案。
C - umi和弓道
题意:在 \(Oxy\) 平面上给一个原点 \((x_0,y_0)\) ,要在原点射箭(射线)出去。再给 \(n\) 个位置两两不同也和原点不同的点作为箭靶,且这 \(n+1\) 个点都不在坐标轴上。在坐标轴上画一条尽可能短的隔板 \(seg\) ,使得原点能射到的箭靶的数量不超过 \(k\) 。
题解:先考虑在 \(x\) 轴上放,那么只能隔开 \(y\) 与原点的 \(y_0\) 异号的箭靶,显然要把他们之间的连线在 \(x\) 轴上的交点覆盖住,所以求出交点之后之间尺取就可以。在 \(y\) 轴上放同理。