第一场
I题 1or2
这个是一般图匹配的题目,需要学习下带花树算法,博客如下
https://blog.csdn.net/weixin_45735431/article/details/107392327
主要难点就是分出虚点,把节点的度不是1的图转为度都为1的图,在进行一般图匹配
至于如何分虚点,博客如下
https://www.cnblogs.com/xiongtao/p/11189452.html
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 2e3 + 10;
const int M = 5e5 + 10;
struct node {
int to, nxt;
} g[M];
int head[N], cnt;
int vis[N], match[N], f[N], pre[N], Id, id[N];
// vis[i]: 0(未染色) 1(黑色) 2(白色)
// match[i]: i的匹配点
// f[i]: i在带花树中的祖先
// pre[i]: i的非匹配边的另一点
// id: 找LCA用
// g数组: 存关系
int n, m, ans, u, v;
queue<int> q;
int U[N], V[N];
int h;
void init() {
Id = ans = cnt = 0;
for (int i = 1; i <= (n+m)*2; i++) {
head[i] = -1, id[i] = match[i] = 0;
}
}
void add(int u, int v) { g[cnt].to = v, g[cnt].nxt = head[u], head[u] = cnt++; }
int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
int lca(int x, int y) {
for (++Id;; swap(x, y)) {
if (x) {
x = find(x);
if (id[x] == Id)
return x;
else
id[x] = Id, x = pre[match[x]];
}
}
}
void blossom(int x, int y, int l) { // l为x,y的最近公共祖先(同一个花)
while (find(x) != l) {
pre[x] = y, y = match[x];
if (vis[y] == 2) vis[y] = 1, q.push(y);
if (find(x) == x) f[x] = l;
if (find(y) == y) f[y] = l;
x = pre[y];
}
}
bool aug(int s) {
for (int i = 1; i <= h; i++) {
vis[i] = pre[i] = 0;
f[i] = i;
}
while (!q.empty()) q.pop();
q.push(s), vis[s] = 1;
while (!q.empty()) {
u = q.front();
q.pop();
for (int i = head[u]; ~i; i = g[i].nxt) {
v = g[i].to;
if (find(u) == find(v) || vis[v] == 2) continue;
if (!vis[v]) {
vis[v] = 2, pre[v] = u;
if (!match[v]) {
for (int x = v, last; x; x = last)
last = match[pre[x]], match[x] = pre[x], match[pre[x]] = x;
return true;
}
vis[match[v]] = 1, q.push(match[v]);
} else {
int LCA = lca(u, v);
blossom(u, v, LCA), blossom(v, u, LCA);
}
}
}
return false;
}
int k;
int d[N];
vector<int> e[N];
void creat() {
for (int i = 1; i < N; i++) e[i].clear();
h = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= d[i]; j++) {
e[i].push_back(++h);
}
}
//h--;
for (int i = 1; i <= m; i++) {
h++;
for (int j = 0; j < e[U[i]].size(); j++) {
add(e[U[i]][j], h);
add(h, e[U[i]][j]);
}
h++;
for (int j = 0; j < e[V[i]].size(); j++) {
add(e[V[i]][j], h);
add(h, e[V[i]][j]);
}
add(h - 1, h);
add(h, h - 1);
}
}
int main() {
while (cin >> n >> m) {
for (int i = 0; i <= cnt; i++) g[i].to = 0, g[i].nxt = 0;
memset(match, 0, sizeof(match));
memset(id, 0, sizeof(id));
memset(f, 0, sizeof(f));
memset(pre, 0, sizeof(pre));
memset(head, 0, sizeof(head));
memset(vis, 0, sizeof(vis));
ans = 0;
k = 0;
init();
for (int i = 1; i <= n; i++) cin >> d[i];
int a = m;
while (a--) {
cin >> u >> v;
U[++k] = u;
V[k] = v;
/*add(u, v);
add(v, u);*/
}
creat();
for (int i = 1; i <= h; i++) {
if (!match[i] && aug(i)) ans++;
}
if (ans == h/2)
cout << "Yes";
else
cout << "No";
cout << endl;
}
return 0;
}
J题 Easy Integration
第一场的数论题之一,名字说是easy,但是推公式还是有点麻烦
题意大概是让你求对(x-x*x)^n积分
这个可以通过分部积分推公式
给一下大佬的推算过程
博客为
https://blog.csdn.net/qq_44607936/article/details/107308777?%3E
然后就是求这个式子了
我们需要求n!,这个前缀和处理一下,注意下乘法取模
然后对于除法,使用费马小定理取模就可以了
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int maxn = 2e6 + 10;
ll q_pow(ll a, ll b) {
ll sum = 1;
while (b) {
if (b & 1) {
sum = (sum * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return sum % mod;
}
int n;
ll f[maxn];
int main() {
f[0] = 1;
for (int i = 1; i <= maxn; i++) {
f[i] = (f[i - 1] * i) % mod;
}
while (cin >> n) {
cout << (((f[n] * f[n]) % mod) * q_pow(f[2 * n + 1], mod - 2)) % mod << endl;
}
return 0;
}
第二场
B题 Boundary
这一题的大概意思就是给你一个原点(0,0)和一些点,问画一个圆(一定过原点),在园的边界上最多可以有多少点
这里有一个非常朴素的做法,不需要像题解那样处理高精度问题(据说要处理到1e-10),也是这题大多数的做法
我们具体做法通过三点确定一条直线来2个点2个点地画圆(两个for循环即可),然后使用map数组来维护圆心,如果遇到了相同的圆点,我们就把map数组加1
至于对于顶点是否会重复访问,我们只需要在第一个for循环里面重置map数组即可,简略证明如下
当我们选定一个节点,并通过这个节点在第二个for循环找第三个节点(还有个原点),这时候,我们对于一个园,当求出一个圆心之后,我们知道了圆心,原点,第一个循环中的点三个点,确定了一个园,所以在第二重循环中一个点所在的圆下个第一次循环就不会再出现
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int maxn = 2e3 + 10;
int n;
typedef struct {
double x;
double y;
}P;
typedef pair<double, double> p;
map<p, int> Map;
P circle[maxn];
p get_circle(double x1, double y1, double x2, double y2, double x3, double y3) {
double a = 2 * (x2 - x1);
double b = 2 * (y2 - y1);
double c = x2 * x2 + y2 * y2 - x1 * x1 - y1 * y1;
double d = 2 * (x3 - x2);
double e = 2 * (y3 - y2);
double f = x3 * x3 + y3 * y3 - x2 * x2 - y2 * y2;
double x = (b * f - e * c) / (b * d - e * a);
double y = (d * c - a * f) / (b * d - e * a);
return p{x, y};
}
bool cmp(P a, P b) {
if (a.x != b.x)
return a.x < b.x;
else
return a.y < b.y;
}
double x, y;
int ans = 0;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> x >> y;
circle[i].x = x;
circle[i].y = y;
}
sort(circle + 1, circle + n + 1, cmp);
for (int i = 1; i <= n; i++) {
Map.clear();
for (int j = i + 1; j <= n; j++) {
if (circle[i].x * circle[j].y == circle[i].y * circle[j].x) continue;
p point = get_circle(0.0, 0.0, circle[i].x, circle[i].y, circle[j].x,
circle[j].y);
ans = max(ans, ++Map[point]);
}
}
cout << ans + 1;
return 0;
}
C题 Cover the Tree
这题是给定一个树,询问要最少多少条链可以把这个树变成环
这是一道dfs序的题目,博客如下
https://blog.csdn.net/weixin_44235989/article/details/107332926?%3E
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
int n;
vector<int> g[maxn];
int num[maxn];
int sum;
int used[maxn];
void dfs(int s) {
if (g[s].size() == 1) {
sum++;
num[sum - 1] = s;
}
for (int i = 0; i < g[s].size(); i++) {
if (!used[g[s][i]]) {
used[g[s][i]] = 1;
dfs(g[s][i]);
}
}
return;
}
int main() {
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
used[1] = 1;
dfs(1);
cout << (sum + 1) / 2 << endl;
for (int i = 0; i < (sum + 1) / 2; i++) {
cout << num[i] << ' ' << num[i + sum / 2] << endl;
}
return 0;
}
第四场
H题 Harder Gcd Problem
这一题题意大概是给定一个数n,求小于等于n的数所组成gcd(i,j)>1的最多对数,并且输出这些数对
对于这一题思考,我们很可以发现,
1.每一个素数只能和他的倍数所匹配
2.每个素数的倍数之间均可以两两配对
通过这两点,我们便可以使用贪心的方法做这道题
我们先使用欧拉筛筛出所有的素数,然后从小于n/2的最大素数开始求素数的倍数并且标记,如果在小于等于n范围内这个素数的倍数的个数(包括自身)是偶数,我们就直接加上个数/2;如果是奇数,我们把这个素数的2倍的数标记撤除,空余出这个数
为什么这样做是最优的呢?下面给出简略证明
首先我们通过小于等于n/2最大的素数来求倍数,第一点是只有素数的两倍小于等于n,它才有最少一个匹配数;第二点是越大的素数它的匹配数就越少,所以我们贪心的先解决最大素数的匹配
然后就是为什么我们求素数倍数可以最优。因为当我们求一个素数的倍数,并且从最大的素数开始,这时候每个素数只会遇到两种情况:倍数的个数是偶数和倍数的个数是奇数。如果是偶数就全部两两配对,奇数就留出这个数两倍的数,其他的两两配对。这时候就算我们匹配掉了后面的数,求解到前面奇数的时候,仍然是只会有奇偶个数区别。对于奇数我们把2倍数留下来,最后我们求到2的时候,所有剩下的数都是2的倍数,这样我们除了无法匹配的素数,最多只有1个数无法匹配(2倍数的个数为奇数时)
所以,我们就可以解决这道题,代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
int is_prime[maxn]; //用来筛取的数组,用作标记
int prime[maxn]; //将素数存入其中
int cnt = 0; //记录素数的个数
void sieve() {
for (int i = 2; i < maxn; i++) {
if (!is_prime[i]) prime[cnt++] = i; //没有被标记便存入prime数组
for (int j = 0; j < cnt; j++) {
if (i * prime[j] >= maxn) break; //当筛到最大数时,便停止
is_prime[i * prime[j]] = 1; //标记为非素数
//停止条件,也是欧拉筛的核心
if (i % prime[j] == 0) break;
}
}
}
int T;
int n;
int vis[maxn];
vector<int> List;
int main() {
sieve();
cin >> T;
while (T--) {
List.clear();
memset(vis, 0, sizeof(vis));
cin >> n;
int count = 0;
for (int i = cnt - 1; i >= 0; i--) {
if (prime[i] <= n / 2) {
int ans = 0;
for (int j = 1; j * prime[i] <= n; j++) {
if (!vis[j * prime[i]]) {
vis[j * prime[i]] = 1;
ans++;
List.push_back(j * prime[i]);
}
}
if (ans % 2 == 1) {
vis[List[List.size() - ans + 1]] = 0;
List[List.size() - ans + 1] = List[List.size() - 1];
List.pop_back();
}
count += ans / 2;
}
}
printf("%d\n", count);
for (int i = 0; i < List.size(); i += 2) {
printf("%d %d\n", List[i], List[i + 1]);
}
}
return 0;
}
第6场
B Binary Vector
这一题是一道数论题,给上公式的推导
最后得到f的递推式。然后需要注意的是,求1/(2^i)的逆元时,费马小定理超时,我们在这里这样x = x * y % mod;
来计算
for (int i = 2; i <= 2e7; i++) {
x = x * y % mod;
ans[i] = ans[i - 1] * 2;
a[i] = ((a[i - 1] - a[i - 1] * x) % mod + mod) % mod;
}
然后输出f1 ^ f2 ^ …… ^ fn的值即可
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn = 2e7 + 10;
const ll mod = 1e9 + 7;
int T;
int n;
ll a[maxn];
ll ans[maxn];
ll x = 5e8 + 4;
ll y = 5e8 + 4;
ll qpow(ll a, ll b) {
ll sum = 1;
while (b) {
if (b & 1) {
sum = sum * a % mod;
}
a = a * a % mod;
b /= 2;
}
return sum % mod;
}
int main() {
cin >> T;
a[1] = 500000004;
ans[1] = 1;
for (int i = 2; i <= 2e7; i++) {
x = x * y % mod;
ans[i] = ans[i - 1] * 2;
a[i] = ((a[i - 1] - a[i - 1] * x) % mod + mod) % mod;
}
for (int i = 2; i <= 2e7; i++) {
a[i] = a[i - 1] ^ a[i];
}
while (T--) {
cin >> n;
cout << a[n] << endl;
}
return 0;
}
第八场
I Interesting Computer Game
题意大概是:给出n对数,你可以操作n次,每次操作只能在下面三种中选择一种,问最多可以选多少个不同的数字。
- 什么都不做
- 如果a[i]以前没选过,那么可以选择a[i]
- 如果b[i]以前没选过,那么可以选择b[i]
对于这一题,我们可以将其转换为图来解决。
题目要求我们找出最多的不同数字,如果我们把ai和bi连起来,我们可以构成多个联通图。这个时候,就是在要求我们求出这些联通图最多的增广路
同时我们注意到,对于一个连通图,如果图中存在至少一个环,那么我们便可以选择这个联通图的所有顶点;如果不存在环,我们只能选择这个图顶点数减1个点。所以这题又转变为联通图记录顶点数加判环的问题
至于判环,这题有两种解决方式。
1.通过并查集来高效判环
2.通过dfs来判环
这里是使用第二种方法,具体就是在寻找增广路的过程中,如果遇到一个被标记过的点,并且上一个点不是从它转换过来的(用pre数组标记这个点是从哪个点寻找过来的),那么就是出现了环
然后需要对于数据进行一下离散化,这里使用unordered_map来离散化处理
代码如下
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 10;
unordered_map<ll, ll> a;
int vis[maxn];
int T;
int n;
int cnt;
ll num1[maxn], num2[maxn];
vector<int> vec[maxn];
int sum;
int ans;
int flag;
int pre[maxn];
void dfs(int x) {
ans++;
for (int i = 0; i < vec[x].size(); i++) {
if (!vis[vec[x][i]]) {
vis[vec[x][i]] = 1;
pre[vec[x][i]] = x;
dfs(vec[x][i]);
} else if (pre[x] != vec[x][i]) {
flag = 1;
}
}
}
int main() {
cin >> T;
int h = T;
while (T--) {
memset(pre, 0, sizeof(pre));
memset(vis, 0, sizeof(vis));
for (int i = 1; i < maxn; i++) {
vec[i].clear();
}
a.clear();
scanf_s("%d", &n);
sum = 0;
cnt = 0;
for (int i = 1; i <= n; i++) {
scanf_s("%d%d", &num1[i], &num2[i]);
if (!a.count(num1[i])) {
a.emplace(num1[i], ++cnt);
}
if (!a.count(num2[i])) {
a.emplace(num2[i], ++cnt);
}
}
for (int i = 1; i <= n; i++) {
vec[a[num1[i]]].push_back(a[num2[i]]);
vec[a[num2[i]]].push_back(a[num1[i]]);
}
for (int i = 1; i <= cnt; i++) {
if (!vis[i]) {
vis[i] = 1;
flag = 0;
ans = 0;
dfs(i);
if (flag == 1) {
sum += ans;
} else
sum += (ans - 1);
}
}
printf("Case #%d: %d\n", h - T, sum);
}
return 0;
}