1.标题统计
算法分析
由于算法竞赛中已经用不了 g e t s gets gets函数,可以考虑用 g e t c h a r getchar getchar或 s t r i n g string string。因为字符串有空格,不能用 s c a n f scanf scanf读入,因为 s c a n f scanf scanf读入时遇到空格就停止了。题面说只有一行字符串,但是包括换行符,因此换行符只能是文末换行符。正好用 g e t c h a r getchar getchar读入时可以判断是否结束。
#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;
char s[10];
int main()
{
int n= 0;
while ( ( s[++n] = getchar() ) != '\n' );
s[n] = '\0';
--n;
int num = 0;
for (int i = 1; i <= n; ++i)
if (s[i] == ' ') ++num;
printf("%d\n", n - num);
return 0;
}
算法拓展
用 s t r i n g string string。 s t r i n g string string一般比字符数组慢些,平常不太推荐给学生用。但是处理规模小些的字符串的时候确实好用。需要包含头文件 < s t r i n g > <string> <string>。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
string s;
int main()
{
getline(cin, s); // 读入一行,遇到换行符停止
int len = s.length(); // 字符串s的长度
int num = 0;
for (int i = 0; i < len; ++i)
if (s[i] == ' ') ++num;
printf("%d\n", len - num);
return 0;
}
2.龙虎斗
算法分析
题意明显,模拟就行。以 m m m为分界,先算出龙方的气势值为 s u m 1 sum1 sum1和虎方的气势值为 s u m 2 sum2 sum2。然后枚举兵营,一个个判断比较得出结果。
本题需要开 l o n g l o n g long \quad long longlong。防止结果溢出和计算过程中的乘法溢出。结果溢出好发现,过程中的乘法溢出不好发现。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
ll c[100010], s1, s2;
int n, m, p1;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%lld", &c[i]);
scanf("%d%d%lld%lld", &m, &p1, &s1, &s2);
ll sum1 = 0, sum2 = 0;
c[p1] += s1;
for (int i = 1; i < m; ++i) sum1 += c[i] * (m - i);
for (int i = m + 1; i <= n; ++i) sum2 += c[i] * (i - m);
int p2;
ll minval = 1e18;
for (int i = 1; i < m; ++i)
{
if (abs(sum1 + s2 * (m - i) -sum2) < minval)
{
minval = abs(sum1 + s2 * (m - i) -sum2);
p2 = i;
}
}
for (int i = m; i <= n; ++i)
{
if (abs(sum2 + s2 * (i - m) - sum1) < minval)
{
minval = abs(sum2 + s2 * (i - m) - sum1);
p2 = i;
}
}
printf("%d\n", p2);
return 0;
}
算法拓展
上述算法已经能够解决问题。这里介绍另外一种,主要是借助数学推理加速,开阔思路。建议考场上在时间复杂度允许的情况下,算法越简单越好。
以 m m m为分界,先算出龙方的气势值为 s u m 1 sum1 sum1和虎方的气势值为 s u m 2 sum2 sum2。
如果 s u m 1 = s u m 2 sum1 = sum2 sum1=sum2,结果为 m m m。
如果 s u m 1 > s u m 2 sum1 > sum2 sum1>sum2,一定是在 m m m的右侧布置工兵即虎方。设 k = ( s u m 1 − s u m 2 ) / s 2 k = (sum1 - sum2) / s2 k=(sum1−sum2)/s2,则布置在 m + k m+k m+k是使 s u m 2 sum2 sum2和 s u m 1 sum1 sum1最接近的位置,布置在 m + k + 1 m+k+1 m+k+1时 s u m 2 sum2 sum2要大于 s u m 1 sum1 sum1了。比较这两个位置,哪个位置的双方差距最小就选哪个位置。如果此时 m + k > = n m + k>=n m+k>=n了,则 n n n是答案。
s u m 1 < s u m 2 sum1 < sum2 sum1<sum2的情况类似。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
ll c[100010], s1, s2;
int n, m, p1;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%lld", &c[i]);
scanf("%d%d%lld%lld", &m, &p1, &s1, &s2);
ll sum1 = 0, sum2 = 0;
c[p1] += s1;
for (int i = 1; i < m; ++i) sum1 += c[i] * (m - i);
for (int i = m + 1; i <= n; ++i) sum2 += c[i] * (i - m);
if (sum1 == sum2) printf("%d\n", m);
else if (sum1 > sum2)
{
int k = (sum1 - sum2) / s2;
if (m + k >= n) printf("%d\n", n);
else if (abs(sum2 + s2*k - sum1) <= abs(sum2 + s2*(k+1) - sum1)) printf("%d\n", m + k);
else printf("%d\n", m + k + 1);
}else if (sum1 < sum2)
{
int k = (sum2 - sum1) / s2;
if (m - k <= 1) printf("1\n");
else if (abs(sum1 + s2*(k+1) - sum2) <= abs(sum1 + s2*k - sum2)) printf("%d\n", m - k - 1);
else printf("%d\n", m - k);
}
return 0;
}
3.摆渡车
算法分析
题意求的是最小的等车时间,不用关心具体发了多少趟车。假如在时间
i
i
i时发了一趟车,那么上一趟车发车的最晚时间是
i
−
m
i-m
i−m,晚于这个时间就不能保证车能在时间
i
i
i返回再发车。上一趟车发车的最早时间是多少呢?为
i
−
2
∗
m
+
1
i-2*m+1
i−2∗m+1。如下图。
假设上一趟车在
i
−
2
∗
m
i-2*m
i−2∗m时发车,则它能在
i
−
m
i-m
i−m返回还能发一趟车,这样在时间
i
i
i发车的时候又多发了一趟车。因此,上一趟车发车的区间范围是
[
i
−
2
∗
m
+
1
,
i
−
m
]
[i-2*m+1, i -m]
[i−2∗m+1,i−m]。动态规划的思路好想。
f [ i ] : f[i]: f[i]:在时间 i i i发一次车的最小总等待时间。枚举上次发车的时间 j j j为决策,时间区间 [ j + 1 , i ] [j+1, i] [j+1,i]内的人都让时间 i i i发的车接走。
f [ i ] = m i n ( f [ i ] , f [ j ] + s s u m ( j + 1 , i ) ) f[i] = min(f[i], f[j] + ssum(j+1, i)) f[i]=min(f[i],f[j]+ssum(j+1,i))
s s u m ( j + 1 , i ) ssum(j+1, i) ssum(j+1,i)表示区间 [ j + 1 , i ] [j+1, i] [j+1,i]内的人都让时间 i i i发的车接走的等待时间。这里借助前缀和可以快速计算。假设这段时间内有3个人等车,开始等车的时间用 t [ i ] t[i] t[i]记录,三个人的开始等车时间为 t [ x 1 ] 、 t [ x 2 ] 、 t [ x 3 ] t[x1]、t[x2]、t[x3] t[x1]、t[x2]、t[x3],等待时间 a n s ans ans为:
a
n
s
+
=
t
[
i
]
−
t
[
x
1
]
ans += t[i] - t[x1]
ans+=t[i]−t[x1]
a
n
s
+
=
t
[
i
]
−
t
[
x
2
]
ans += t[i] - t[x2]
ans+=t[i]−t[x2]
a
n
s
+
=
t
[
i
]
−
t
[
x
3
]
ans += t[i] - t[x3]
ans+=t[i]−t[x3]
合并为: a n s = 3 ∗ t [ i ] − ( t [ x 1 ] + t [ x 2 ] + t [ x 3 ] ) ans = 3 * t[i] - (t[x1] + t[x2] + t[x3]) ans=3∗t[i]−(t[x1]+t[x2]+t[x3])
用 s u m 1 [ i ] sum1[i] sum1[i]表示开始等车时间的前缀和, s u m 2 [ i ] sum2[i] sum2[i]表示等车人数的前缀和。
int ssum(int l, int r) // 时间段[l, r]的人由r时间发的车拉完
{
return r * (sum2[r] - sum2[l-1]) - (sum1[r] - sum1[l-1]);
}
于是时间复杂度 O ( t m ) O(tm) O(tm)的70分dp做法就有了。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define ll long long
using namespace std;
int f[4000010], sum1[4000010], sum2[4000010], vis[4000010];
// sum1[i]:0~i时间内开始等待时间的前缀和 sum2[i]:0~i时间内人数前缀和
int t[510];
int ssum(int l, int r) // 时间段[l, r]的人由r时间发的车拉完
{
return r * (sum2[r] - sum2[l-1]) - (sum1[r] - sum1[l-1]);
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", &t[i]);
int r, send = 0;
for (int i = 1; i <= n; ++i) send = max(send, t[i]);
for (int i = 1; i <= n; ++i) ++vis[t[i]];
r = send + m - 1; // 最后一辆车的最晚出发时间
sum1[0] = 0; sum2[0] = vis[0];
for (int i = 1; i <= r; ++i) sum1[i] =sum1[i-1] + i * vis[i];
for (int i = 1; i <= r; ++i) sum2[i] =sum2[i-1] + vis[i];
memset(f, 0x3f, sizeof(f));
for (int i = 0; i < m; ++i) f[i] = i * sum2[i] - sum1[i];
for (int i = m; i <= r; ++i)
for (int j = max(0, i - 2 * m + 1); j <= i - m; ++j)
f[i] = min(f[i], f[j] + ssum(j+1, i));
int ans = 1e8;
for (int i = send; i <= r; ++i) ans = min(ans, f[i]);
printf("%d\n", ans);
return 0;
}
可以用斜率优化dp。先写出完整的转移方程:
f [ i ] = m i n { f [ j ] + i ∗ ( s u m 2 [ i ] − s u m 2 [ j ] ) − ( s u m 1 [ i ] − s u m 1 [ j ] ) } f[i] = min \{ f[j] + i * (sum2[i] - sum2[j]) - (sum1[i] - sum1[j]) \} f[i]=min{f[j]+i∗(sum2[i]−sum2[j])−(sum1[i]−sum1[j])}
这种带有前缀和、 i i i和 j j j共同组成的表达式项的dp,可以考虑斜率优化。
1.去掉 m i n min min和括号。
f [ i ] = f [ j ] + i ∗ s u m 2 [ i ] − i ∗ s u m 2 [ j ] − s u m 1 [ i ] + s u m 1 [ j ] f[i] = f[j] + i * sum2[i] - i * sum2[j] - sum1[i] + sum1[j] f[i]=f[j]+i∗sum2[i]−i∗sum2[j]−sum1[i]+sum1[j]
2.移项。只和 j j j有关的当作因变量,和 j j j和 i i i有关的当作自变量。
f [ j ] + s u m 1 [ j ] = i ∗ s u m 2 [ j ] + f [ i ] + s u m 1 [ i ] − i ∗ s u m 2 [ i ] f[j] + sum1[j] = i * sum2[j] + f[i] + sum1[i] - i * sum2[i] f[j]+sum1[j]=i∗sum2[j]+f[i]+sum1[i]−i∗sum2[i]
观察以上方程式,斜率是 i i i,单调递增,自变量是 s u m 2 [ j ] sum2[j] sum2[j],也单调递增。当截距最小时, f [ i ] f[i] f[i]就能取到最小值。维护下凸壳。需要注意自变量相等的情况,关注代码中的 s r a t i o sratio sratio函数。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define ll long long
using namespace std;
ll vis[4000010];
ll f[4000010], sum1[4000010], sum2[4000010];
// sum1[i]:0~i时间内开始等待时间的前缀和 sum2[i]:0~i时间内人数前缀和
ll t[510], q[4000010], L, R;
double sratio(int a, int b)
{
if (sum2[a] == sum2[b]) return 1e9 * (f[b] + sum1[b] - f[a] - sum1[a]);
else return 1.0 * (f[b] + sum1[b] - f[a] - sum1[a]) / (sum2[b] - sum2[a]);
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%lld", &t[i]);
sort(t + 1, t + n + 1);
for (int i = 1; i <= n; ++i) ++vis[t[i]];
int r = t[n] + m - 1; // 最后一辆车的最晚出发时间
sum1[0] = 0; sum2[0] = vis[0];
for (int i = 1; i <= r; ++i) sum1[i] =sum1[i-1] + i * vis[i];
for (int i = 1; i <= r; ++i) sum2[i] =sum2[i-1] + vis[i];
memset(f, 0x3f, sizeof(f));
L = 1; R = 0;
for (int i = 0; i <= m - 1; ++i) f[i] = i * sum2[i] - sum1[i];
for (int i = m; i <= r; ++i)
{
if (i - m >= 0)
{
while ( L < R && (sratio(q[R-1], q[R]) >= sratio(q[R], i - m)) ) --R;
q[++R] = i - m;
}
while (L < R && sratio(q[L], q[L+1]) <= i || (L <= R && ( q[L] < i - 2 * m + 1))) ++L; // 队列中至少得有两个点
f[i] = f[q[L]] + i * (sum2[i] - sum2[q[L]]) - (sum1[i] - sum1[q[L]]); // 队首是最优决策
}
ll ans = 1e18;
for (int i = t[n]; i <= r; ++i) ans = min(ans, f[i]);
printf("%lld\n", ans);
return 0;
}
算法拓展
1.有人很强,用记忆化搜索过了。对于第 i i i个人,能拉走他的车的发车时间范围是 [ t [ i ] , t [ i ] + m − 1 ] [ t[i], t[i]+ m -1 ] [t[i],t[i]+m−1],下一个车的发车时间范围是 [ t [ i ] + m , t [ i ] + 2 ∗ m − 1 ] [t[i] + m, t[i] + 2 * m -1] [t[i]+m,t[i]+2∗m−1]。用 d f s ( i n t i , i n t s t ) dfs(int\, i,int \, st) dfs(inti,intst)表示规划到了第 i i i个人,拉走他的车的最早发车时间是 s t st st的总的最小等车时间。则对于第 i i i个人来说,设拉走他的最早的车的出发时间是 s t st st,最晚发车时间是 t [ i ] + 2 ∗ m − 1 t[i] + 2 * m -1 t[i]+2∗m−1。
while (j<=n && t[j]<=st) sum+=t[j++];
// 第j个人让下一趟车拉走,[i,j-1]本次拉走,本次拉走时间是st
int best=st*(j-i)-sum+dfs(j,st+m);
然后枚举最早的发车时间 t [ j ] t[j] t[j]。
for (;j<=n;j++) // 枚举发车时间:t[j],下一辆车从t[j] + m开始发车
{
sum+=t[j];
if (t[j] - t[i] > 2 * m - 1) break; // 优化
best=min(t[j]*(j-i+1)-sum+dfs(j+1,t[j]+m),best);
}
从这两步中取最小的 b e s t best best值。
mem[i][st-t[i]]=best
m e m [ i ] [ j ] : mem[i][j]: mem[i][j]:表示第 i i i个人距离最早发车时间的时间差为 j j j的总的最小等车时间。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cctype>
using namespace std;
int n,m,t[505],mem[505][110];
//因为0<=st-t[i]<=m,因此可以记忆化,把这个作为状态的第二维
int dfs(int i,int st)//记忆化搜索。i:第i个人,st:开始的发车时间st
{
if (i==n+1) return 0; //所有人都上车了
if (st<t[i]) return dfs(i,t[i]); //如果现在的时间没有人,就到下一个人的到达时间
if (mem[i][st-t[i]]) return mem[i][st-t[i]]; //记忆化
int sum=0,j=i;
//车等人,开始的发车时间是:st
while (j<=n && t[j]<=st) sum+=t[j++];
int best=st*(j-i)-sum+dfs(j,st+m);
//人等车
for (;j<=n;j++) // 枚举发车时间:t[j],下一辆车从t[j] + m开始发车
{
sum+=t[j];
if (t[j] - t[i] > 2 * m - 1) break; // 优化
best=min(t[j]*(j-i+1)-sum+dfs(j+1,t[j]+m),best);
}
return mem[i][st-t[i]]=best;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1;i<=n;i++) scanf("%d", &t[i]);
sort(t+1,t+n+1);//显然从小到大按照时间排序更好算
cout << dfs(1,0) << endl;
return 0;
}
2.二维dp。可以自行探索。
链接:博客学习
4.对称二叉树
算法分析
树上的问题。据说解决这个问题的算法很多。建树的时候,如果左或右儿子没有了,则相应置为0。再建立一棵树,和原树是左右相反的。现在想办法对这两棵树的子树hash出一个值。取四个质数 p 1 、 p 2 、 p 3 、 p p1、p2、p3、p p1、p2、p3、p,左儿子的 v a l val val乘以 p 1 p1 p1,子树结点本身的 v a l val val乘以 p 2 p2 p2,右儿子的 v a l val val乘以 p 3 p3 p3,相加的结果再整体模 p p p。
最后逐一比较两棵树相同结点的左和右儿子 v a l val val如果相同,则该子树是对称二叉树。有一个点不知道为什么过不去,后来四个质数全改为大质数就过了。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
const int P = 899997942001179;
int n, ans;
struct node
{
int lchild, rchild, val, size;
}stree1[1000010], stree2[1000010];
void dfs(int u)
{
if (u == 0) return;
stree2[u].lchild = stree1[u].rchild;
stree2[u].rchild = stree1[u].lchild;
stree2[u].val = stree1[u].val;
dfs(stree1[u].lchild);
dfs(stree1[u].rchild);
}
void dfs1(int u)
{
if (u == 0) return;
if (stree1[u].lchild == 0 && stree1[u].rchild == 0)
{
stree1[u].size = 1; return;
}
dfs1(stree1[u].lchild);
dfs1(stree1[u].rchild);
stree1[u].size = stree1[stree1[u].lchild].size + stree1[stree1[u].rchild].size + 1;
stree1[u].val = (stree1[stree1[u].lchild].val * 999999751 + stree1[u].val * 299999827 + stree1[stree1[u].rchild].val * 100000007) % P;
}
void dfs2(int u)
{
if (u == 0) return;
if (stree2[u].lchild == 0 && stree2[u].rchild == 0)
{
stree2[u].size = 1; return;
}
dfs2(stree2[u].lchild);
dfs2(stree2[u].rchild);
stree2[u].size = stree2[stree2[u].lchild].size + stree2[stree2[u].rchild].size + 1;
stree2[u].val = (stree2[stree2[u].lchild].val * 999999751 + stree2[u].val * 299999827 + stree2[stree2[u].rchild].val * 100000007) % P;
}
void dfs3(int u)
{
if (stree1[u].lchild == 0 && stree1[u].rchild == 0) return;
if ( stree1[stree1[u].lchild].val == stree2[stree2[u].lchild].val && stree1[stree1[u].rchild].val == stree2[stree2[u].rchild].val) ans = max(ans, stree1[u].size);
dfs3(stree1[u].lchild);
dfs3(stree1[u].rchild);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &stree1[i].val);
int a, b;
for (int i = 1; i <= n; ++i)
{
scanf("%d%d", &a, &b);
if (a != -1) stree1[i].lchild = a;
if (b != -1) stree1[i].rchild = b;
}
// stree2
dfs(1);
// val, size
dfs1(1); dfs2(1);
// 比较
ans = 1;
dfs3(1);
printf("%d\n", ans);
return 0;
}
算法拓展
1.对每个结点分别以“左、中、右”和“右、中、左”hash出一个值,方法和上述一致。然后比较即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define ull unsigned long long
using namespace std;
const ull P = 899997942001179;
int n;
ull ans;
struct node
{
ull lchild, rchild, val, size;
}stree[1000010];
ull svall[1000010], svalr[1000010];
void dfs(int u)
{
if (stree[u].lchild) dfs(stree[u].lchild);
if (stree[u].rchild) dfs(stree[u].rchild);
stree[u].size = stree[stree[u].lchild].size + stree[stree[u].rchild].size + 1;
svall[u] = (svall[stree[u].lchild] * 999999751 + stree[u].val * 299999827 + svall[stree[u].rchild] * 100000007) % P;
svalr[u] = (svalr[stree[u].rchild] * 999999751 + stree[u].val * 299999827 + svalr[stree[u].lchild] * 100000007) % P;
}
void dfs1(int u)
{
if (svall[stree[u].lchild] == svalr[stree[u].rchild])
{
ans = max(ans, stree[u].size);
}
if (stree[u].lchild) dfs1(stree[u].lchild);
if (stree[u].rchild) dfs1(stree[u].rchild);
}
int main()
{
// freopen("P5018_17.in", "r", stdin);
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%llu", &stree[i].val);
int a, b;
for (int i = 1; i <= n; ++i)
{
scanf("%d%d", &a, &b);
if (a != -1) stree[i].lchild = a;
if (b != -1) stree[i].rchild = b;
}
// svall, svalr
dfs(1);
// query
ans = 1;
dfs1(1);
printf("%llu\n", ans);
return 0;
}
2.直接暴力比对。
(1)枚举根节点,判断其左右两个孩子节点 是否存在 以及 是否相等. 若存在并且点权相等,则一直递归左右两个孩子节点的左右两个孩子节点 。重复上述判断。
(2)判断好对称二叉树后,就可以计算以该节点为根节点的对称二叉子树的节点数量并取最优值了。
以下是别人代码,简单易懂。
#include <bits/stdc++.h>
#define REG register
using namespace std;
typedef long long LL;
const int kN = 1e6 + 10;
int v[kN], l[kN], r[kN];
//v[i]:节点i权值,l[i]:编号为i的节点的左孩子的编号
//r[i]:编号为i的节点的右孩子的编号
int N, ans = 0;
bool pd; //判断是否为对称二叉子树
int cnt(int x) { //计算以x为根节点的对称二叉子树的节点数
int sum = 0;
if (l[x] != -1) sum += cnt(l[x]);
if (r[x] != -1) sum += cnt(r[x]);
return sum + 1; //别忘了根节点
}
void check(int x, int y) { //判断对称二叉子树
if (x == -1 && y == -1) return ; //如果已经到底了,结束
if (x == -1 || y == -1 || v[x] != v[y]) { //不对称
pd = false; return ;
}
check(l[x], r[y]);
check(r[x], l[y]); //这里代码后插图另作解释
}
int main() {
scanf("%d", &N);
for (REG int i = 1; i <= N; ++i)
scanf("%d", &v[i]);
for (REG int i = 1; i <= N; ++i)
scanf("%d%d", &l[i], &r[i]);
ans = 1; //至少有一个对称(一个节点)
for (REG int i = 1; i <= N; ++i) { //枚举对称二叉子树的根节点
if (l[i] != -1 && r[i] != -1 && v[l[i]] == v[r[i]]) {
pd = true; //先默认为是对称二叉子树
check(l[i], r[i]);
if (pd) ans = max(ans, cnt(i)); //如果是对称二叉子树就可以计算节点数取最大值了
}
}
printf("%d\n", ans);
return 0;
}