目录
牛客竞赛2021年度训练联盟热身训练赛第一场
A - Weird Flecks, But OK
思路分析
看懂这个题目之后第一个想到的就是投影到三个坐标面分别求圆,最后看哪个坐标面求出的圆的直径小。
但是一个面内一个圆如何囊括住所有点,而且直径最小,我和同伴有过好几种想法,但都不太行,我最后一个思路是遍历点,看这个点是否在之前定好的圆里,如果在则不更新,如果不在那么需要更新圆,当时只是想着用三个点固定圆,来的这个点如果不是在圆中,就让这个点分别和三个点中的两个点做圆,但是后来想了想这样肯定是不对的,因为在新的圆不能保证把所有点囊括进去。
后来看了网上的解析,每次更新的时候都要一个一个点去判断,不能直接武断地选某几个点。
边输入,便更新圆。第k个点进来的时候,这k个点一定可以被其中两个点连线为直径的圆或者三个点确定的圆全部囊括。有点像枚举出来了。
具体的在代码里了,不好描述,意会意会。
AC代码
#include <iostream>
#include <cmath>
using namespace std;
void solve(int);
struct Point
{
double x;
double y;
double getdisten(Point & b)
{
return sqrt( (x-b.x)*(x-b.x) + (y-b.y)*(y-b.y) );
}
} ps[3][5005]; // ps[0]表示XOY面,ps[1]表示XOZ面,ps[2]表示YOZ面
Point centre[3]; // 每个面圆的中心
double R[3]; // 每个面目囊括所有点的圆
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> ps[0][i].x >> ps[0][i].y >> ps[1][i].y;
ps[1][i].x = ps[0][i].x;
ps[2][i].x = ps[0][i].y;
ps[2][i].y = ps[1][i].y;
solve(i);
}
printf("%.10f\n", 2 * min(min(R[0], R[1]), R[2]));
return 0;
}
Point midpoint(Point p1, Point p2)
{
return (Point){(p1.x+p2.x)/2, (p1.y+p2.y)/2};
}
double getcentre(Point a, Point b, Point c, Point & centre) // 获取a,b,c三点确定的圆,存到centre中,返回半径
{
double upx1 = (a.x*a.x - b.x*b.x + a.y*a.y - b.y*b.y) * (a.y-c.y);
double upy1 = (a.x*a.x - b.x*b.x + a.y*a.y - b.y*b.y) * (a.x-c.x);
double upx2 = (a.x*a.x - c.x*c.x + a.y*a.y - c.y*c.y) * (a.y-b.y);
double upy2 = (a.x*a.x - c.x*c.x + a.y*a.y - c.y*c.y) * (a.x-b.x);
double dowx = 2*(a.x - b.x)*(a.y - c.y) - 2*(a.x - c.x)*(a.y - b.y);
double dowy = 2*(a.y - b.y)*(a.x - c.x) - 2*(a.y - c.y)*(a.x - b.x);
centre.x = (upx1 - upx2) / dowx;
centre.y = (upy1 - upy2) / dowy;
return centre.getdisten(a);
}
void solve(int k) // 第一个点
{
for (int i = 0; i < 3; i++) // 第i个面
{
if (centre[i].getdisten(ps[i][k]) > R[i])
{
centre[i] = ps[i][k];
R[i] = 0;
for (int j = 1; j < k; j++) // 第二个点
{
if (centre[i].getdisten(ps[i][j]) > R[i])
{
centre[i] = midpoint(ps[i][k], ps[i][j]); // 两点的中点
R[i] = centre[i].getdisten(ps[i][j]); // 以这两点连线为直径的圆的半径
for (int f = 1; f < j; f++) // 第三个点
//如果遍历完f,发现都在之前定的圆里,那个圆对[1,j-1]上的所有点符合要求
//否则三个点定一个圆
{
if (centre[i].getdisten(ps[i][f]) > R[i])
{
R[i] = getcentre(ps[i][f], ps[i][k], ps[i][j], centre[i]); // 三个点求圆心
}
}
}
}
}
}
}
B - Code Names
思路分析
如果互相能通过交换两个字母获得对方,则这两个字符串之间可以连一条线,最终会形成一个图,题意是找到一个字符串集合,使得集合中的字符串相互直接不能通过交换两个字母获得对方,求出符合条件的最大集合,输出其字符串元素的个数。把字符串看作一个结点,相当于求一个图的最大独立集,最大独立集大小 = n - 最大匹配数,这个图不是标准的二分图,所以可以把一个结点分成两个,就形成了两部分,两部分之间相互连线,这时求出的最大匹配数会是两倍,所以最后得到的是最大独立集大小 = n - 二分图最大匹配数 / 2。
类似于这个题,当时理解了很长时间,这次能看出来类型也是不容易。
求二分图的最大匹配数的匈牙利算法代码如下:
bool match(int x) // 匈牙利算法
{
for(int i = 1; i <= n; i++)
{
if(Map[x][i] == 1 && !vis[i])
{
vis[i] = true;
if(p[i] == 0 || match(p[i]))
{
p[i] = x;
return true;
}
}
}
return false;
}
int cnt = 0;
for (int i = 1; i <= n; i++)
{
memset(vis, 0, sizeof(vis));
cnt += match(i);
}
AC代码
#include <iostream>
#include <cstring>
using namespace std;
int n, p[505];
bool vis[505], Map[505][505];
bool swap_free(string & a, string & b); // 返回 0表示互相不能通过交换两个字母获得对方
bool match(int x) // 匈牙利算法
{
for(int i = 1; i <= n; i++)
{
if(Map[x][i] == 1 && !vis[i])
{
vis[i] = true;
if(p[i] == 0 || match(p[i]))
{
p[i] = x;
return true;
}
}
}
return false;
}
int main()
{
string s[505];
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
for (int j = 1; j < i; j++)
{
Map[i][j] = Map[j][i] = swap_free(s[i], s[j]);
}
}
int cnt = 0;
for (int i = 1; i <= n; i++)
{
memset(vis, 0, sizeof(vis));
cnt += match(i);
}
cout << n - cnt/2 << endl;
return 0;
}
bool swap_free(string & a, string & b)
{
int cnt = 0, len = a.size();
for (int i = 0; i < len; i++)
{
if (a[i] != b[i])
{
cnt++;
}
}
if (cnt == 2) return 1;
return 0;
}
C - New Maths
毫无疑问,我做这道题超时了,最后参考了这篇博客,几乎和人家写的一模一样了,一点都改不了全是精华🤣
思路分析
手算一下可以发现如下规律:
设数N的位数为n,可以看出如果n是个偶数,那就不可能有一个数a进行圈乘之后得到N。如果n是奇数,那么a的位数一定是
(
n
+
1
)
/
2
(n+1)/2
(n+1)/2,不妨把a和N都用数组来存每一位的数字,倒着存,方便运算。不妨把圈乘设成
∗
*
∗, 圈加设成
+
+
+。
例如,a=456789 时 a[0]=9,a[1]=8,…,a[5]=4。
可以看出:
n[1] = a[0]
∗
*
∗a[0]
n[2] = a[0]
∗
*
∗a[1]
+
+
+ a[1]
∗
*
∗a[0]
n[3] = a[0]
∗
*
∗a[2]
+
+
+ a[1]
∗
*
∗a[1]
+
+
+ a[2]
∗
*
∗a[0]
n[4] = a[0]
∗
*
∗a[3]
+
+
+ a[1]
∗
*
∗a[2]
+
+
+ a[2]
∗
*
∗a[1]
+
+
+ a[3]
∗
*
∗a[0]
n[5] = a[0]
∗
*
∗a[4]
+
+
+ a[1]
∗
*
∗a[3]
+
+
+ a[2]
∗
*
∗a[2]
+
+
+ a[3]
∗
*
∗a[1]
+
+
+ a[4]
∗
*
∗a[0]
⋯
⋯
\cdots\cdots
⋯⋯
以上的规律其实是特殊的卷积,序列卷积的通用型公式为:
在这道题可以写成:
可以看出
n
[
j
]
n[j]
n[j] 只与
≤
j
≤j
≤j的位上的数有关,所以暂定第一位之后可以求第二位,后面为暂定了之后可以求第三位,
⋯
\cdots
⋯,可以求出后面几位,就能找第
j
j
j 位是什么了。可以深搜,每次搜到底了,就更新一下ans,找最小的答案。具体见AC代码:
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int a[30], b[30], len; // a存了N,b存了a,好像不太好,但懒得改了
ll ans = INF;
bool check(int pos) // 判断目前的b求出N的第pos位上的数对不对
{
ll x = 0;
for (int i = 0; i <= pos; i++)
{
x = (x + (b[i]*b[pos-i])) % 10;
}
return (a[pos] == x);
}
void dfs(int pos)
{
if (pos == len) // 搜到底儿了,len个数都确定了
{
for (int i = len; i < len*2-1; i++) // 先遍历一下符不符合条件
{
if (!check(i))
{
return;
}
}
// 符合条件,要更新ans
ll tmp = 0;
for (int i = len-1; i >= 0; i--)
{
tmp = tmp*10 + b[i];
}
ans = min(ans, tmp);
return;
}
for (int i = 0; i <= 9; i++) // 从0到9遍历一遍,寻找这一位可能的值,进行深搜
{
b[pos] = i;
if (check(pos))
{
dfs(pos+1);
}
}
}
int main()
{
string n;
cin >> n;
len = n.size();
if (len%2 == 1)
{
for (int i = 0; i < len; i++) // 把 n存到 a数组
{
a[i] = n[len - i -1] - '0';
}
len = (len+1)>>1; // 所求的数的位数
dfs(0);
cout << ((ans==INF)?-1:ans) << endl;
return 0;
}
cout << -1 << endl;
return 0;
}
D - Some Sum
思路分析
找规律,签到题
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
int main()
{
int n;
cin >> n;
if (n %2 == 1)
{
cout << "Either" << endl;
}
else
{
if ((n/2)%2 == 0)
{
cout << "Even" << endl;
}
else
{
cout << "Odd" << endl;
}
}
return 0;
}
E - Early Orders
思路分析
第一反应,题面容易理解,但写的时候发现写不上来,ԾㅂԾ,
看了网上的解析,说是单调栈,理解了之后写的代码,啥也不是🙃
思想就是,目前这个数x要压到栈里面,压入之前,要看栈顶元素a是否大于x,大于的话还要看有没有替换的,也就是后面还有没有a,有的话,完全可以选后面的数,如果这个时候a还在栈里的话,说明比x大的数a放到了x的前面,明显比a放到x后面大,要让序列的字母序最小的话,这种时候栈顶元素肯定要弹出的。
#include <iostream>
#include <stack>
using namespace std;
int k, n, p = 0;
// arr存序列,cnt存系列中相同数字的个数。
int arr[200005], cnt[200005], ans[200005];
bool f[200005];
stack<int> st;
int main()
{
st.push(0);
cin >> n >> k;
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
cnt[arr[i]]++;
}
for (int i = 1; i <= n; i++)
{
cnt[arr[i]]--; // 统计的个数要减少一个
if (!f[arr[i]]) // 没有标记过的话,继续
{
while(arr[i] < st.top() && cnt[st.top()] > 0) // 一旦栈顶元素大于arr[i],并且后面栈顶元素个数大于0,也就是后面还有,那就把栈顶元素弹出,这个数选后面点的
{
f[st.top()] = 0; // 把栈顶元素的标记置 0,表示还没选
st.pop(); // 弹出
}
st.push(arr[i]); // 压入
f[arr[i]] = 1; // 做标记
}
}
for (int i = 1; i <= k; i++)
{
ans[++p] = st.top();
st.pop();
}
for (int i = p; i > 0; i--)
{
if (i < p)
{
cout << " ";
}
cout << ans[i];
}
cout << endl;
return 0;
}
F - Pulling Their Weight
要找到一个数ans,整个序列中比ans小的数的和 = = =比ans大的数的和,第一个反应是要排序之后求前缀和,在这动物的体重不大于2000,而动物个数更多,所以我用了桶排序,相当于输入的同时就排好序了。然后一个变量SUM存总体重数,用来辅助求对一个数a,比a小的数的和以及比a大的数的和。
思路就是遍历,找到一个数ans,整个序列中比ans小的数的和 = = =比ans大的数的和,有点废话了,但是就是这么办的。具体见AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 20000;
ll weight[20005], Sum[100005], dif[100005];
int main()
{
ll SUM = 0, p = 0;
int m, w;
cin >> m;
for (int i = 1; i <= m; i++)
{
cin >> w;
SUM += w;
weight[w]++;
}
for (int i = 1; i <= N; i++)
{
if (weight[i])
{
Sum[i] = Sum[p] + weight[i]*i; // 前缀和
if (SUM - Sum[i] == Sum[i]) // 说明小于等于i的数的和 = 大于i的数的和
{
if (!weight[i+1]) // 如果下一个数是空的,那么下一个数就是结果了
{
cout << i+1 << endl;
return 0;
}
}
if (SUM - Sum[i] == Sum[p]) // 如果大于 i的数的和 = 小于i的数的和
{
cout << i << endl; // 符合条件直接输出
return 0;
}
p = i;
}
}
return 0;
}
G - Birthday Paradox
思路分析
纯概率问题,有点复杂,看了网上的解析,纯粹求公式。
对于m个人的生日是n天,这n天中每天的人数分别为ci,求这种情况的概率,公式:
其中
d
i
d_{i}
di是:给
c
i
c_{i}
ci 分组,相同的为一组,分成a组,
d
i
d_{i}
di是每组的大小
具体只能自己理解了🙃
代码写得不够简洁:
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
int n, m = 0, p = 0, c[400], a[400] = {};
ll b[15] = {1, 1};
double getlg(bool f, int x)
{
if (f)
{
double ans = 0;
if (x < 15)
{
ans = log10(b[x]);
}
else
{
ans += log10(b[14]);
for (int i = 15; i <= x; i++)
{
ans += log10(i);
}
}
return ans;
}
else
{
double ans = getlg(1, m);
for (int i = 365-n+1; i <= 365; i++)
{
ans += log10(i);
}
return ans;
}
}
double getans()
{
double ans = -m*log10(365) + getlg(0, 0);
for (int i = 1; i <= n; i++)
{
ans -= getlg(1, c[i]);
}
for (int i = 1; i <= p; i++)
{
ans -= getlg(1, a[i]);
}
return ans;
}
int main()
{
for (int i = 2; i < 15; i++)
{
b[i] = b[i-1] * i;
}
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> c[i];
m += c[i];
}
sort(c+1, c+n+1);
for (int i = 1; i <= n; i++)
{
if (c[i] == c[i-1])
{
a[p]++;
}
else
{
a[++p] = 1;
}
}
printf("%.15f\n", getans());
return 0;
}
新代码看着简短多了:
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
int n, m = 0, p = 0, c[400], a[400] = {};
double lg[40000] = {};
void getlg()
{
for (int i = 2; i <=36500; i++)
{
lg[i] = lg[i-1] + log10(i);
}
}
double getans()
{
double ans = -m*log10(365) + lg[365] - lg[365-n] + lg[m];
for (int i = 1; i <= n; i++)
{
ans -= lg[c[i]];
}
for (int i = 1; i <= p; i++)
{
ans -= lg[a[i]];
}
return ans;
}
int main()
{
getlg();
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> c[i];
m += c[i];
}
sort(c+1, c+n+1);
for (int i = 1; i <= n; i++)
{
if (c[i] == c[i-1])
{
a[p]++;
}
else
{
a[++p] = 1;
}
}
printf("%.15f\n", getans());
return 0;
}
H - On Average They’re Purple
理解明白题意之后就知道是单源最短路问题了,直接用了李煜东的算法竞赛进阶指南里的代码。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
int n, m, tot = 0;
bool v[N];
// 大根堆,pair的第二维为节点编号
// 第一维为dist的相反数
priority_queue< pair<int, int> > q;
void add(int x, int y, int z)
{
ver[++tot]=y;
Next[tot]=head[x];
head[x]=tot;
edge[tot]=z;
}
void dijkstra()
{
memset(d, 0x3f, sizeof(d));
memset(v, 0, sizeof(v));
d[1] = 0;
q.push( make_pair(0, 1) );
while (q.size())
{
int x = q.top().second; q.pop();
if (v[x]) continue;
v[x] = 1;
for (int i = head[x]; i; i = Next[i])
{
int y = ver[i], z = edge[i];
if (d[y] > d[x] + z)
{
d[y] = d[x] + z;
q.push( make_pair(-d[y], y) );
}
}
}
}
int main()
{
int x, y, z = 1;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> x >> y;
add(x, y, z);
add(y, x, z);
}
dijkstra();
printf("%d\n", d[n] - 1);
return 0;
}
I - Full Depth Morning Show
看懂解析了,但是还是不会写😭。。。
J - This Ain’t Your Grandpa’s Checkerboard
签到题
K - Solar Energy
不会写。。。听说是模拟退火