往年天梯赛的题目都可以在这里找到
这次的天梯赛题目风格变化比较大,加上题目比较绕,在L2-4上由于题目理解错误浪费了很多时间,最后导致没时间做其他题了。总结一下,这次的失利一部分是由于读题的能力还差点,一部分还是因为做题太少(这学期计科的课太多了QAQ),对于一些思维性较强的题目(L2-3)没什么思路,最后就是敲代码的速度还是得提高,这样才有充足的时间去思考L3级别的题目。竞赛时的心态也有待调整吧,虽然是第二次打天梯赛了,但是做到L2-4的时候心态还是有点崩,这是不应该的。
大二的竞赛大概是告一段落了,希望趁着暑假的训练,能在大三有所蜕变吧。
赛后教练开了补题的链接,在此记录一下比赛时没能做出来或是没能拿满分的题。
L2-2
一开始将所有秒都记录下来是否有安排,然后遍历一遍,将连续的区间输出,但是在处理边界的时候遇到了一点问题,只得了19分,虽然当时想到了用排序重做,但是后面看另一题去了,也没时间重新再写一遍。
这是我用排序的方法重写的代码,可以得全部的分。
#include<algorithm>
#include<iostream>
#include<stdio.h>
using namespace std;
struct timeslice
{
int begin, end;
} t[1000005];
bool cmp(timeslice a, timeslice b)
{
return a.begin < b.begin;
}
void print(int x)
{
printf("%02d:", x / 3600);
x %= 3600;
printf("%02d:", x / 60);
x %= 60;
printf("%02d", x);
}
int main()
{
bool spc = 0;
int n;
int hh, mm, ss;
char c;
scanf("%d", &n);
getchar();
for(int i = 1; i <= n; i++)
{
scanf("%d%c%d%c%d", &hh, &c, &mm, &c, &ss, &c);
getchar(), getchar(), getchar();
t[i].begin = hh * 3600 + mm * 60 + ss;
scanf("%d%c%d%c%d", &hh, &c, &mm, &c, &ss);
t[i].end = hh * 3600 + mm * 60 + ss;
getchar();
}
//插入两个点便于计算
t[0].begin = -1;
t[0].end = 0;
t[n + 1].begin = 24 * 3600 - 1;
sort(t, t + n + 2, cmp);
for(int i = 0; i <= n; i++)
{
if(t[i].end != t[i + 1].begin)
{
if(spc) cout << "\n";
else spc = 1;
int l = t[i].end, r = t[i + 1].begin;
print(l);
cout << " - ";
print(r);
}
}
return 0;
}
L2-3
当时看到这题想到了LCA,但是想了想感觉不太好做,就跳过了先去做L2-4了,现在看别人的AC代码确实用到了LCA的想法,只是用总权值乘2减去最大的点这里我没想到。不过当时如果用暴力的话应该也能过一些点,说不定总分就上175了。
简述一下做法:
在开始的时候预处理每个点到根的距离。每次加入一个点的时候就向上找没被访问过的结点,每遍历一个结点就将总权值加一,这里用到了LCA的想法。最后将总权值乘2再减去最大的权值点即可。
下面是AC的代码:
#include<algorithm>
#include<cstdio>
using namespace std;
const int N = 2e5 + 7;
int w[N], fa[N], vis[N];
int getw(int x)
{
if(w[x] == 0 && fa[x] != -1) w[x] = getw(fa[x]) + 1;
return w[x];
}
int main()
{
bool spc = 0;
int n, m, x, sum = 0, far;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
vis[i] = w[i] = 0;
scanf("%d", &x);
fa[i] = x;
if(x == -1)
{
vis[i] = 1;
far = i;
}
}
for(int i = 1; i <= n; i++)
w[i] = getw(i);
while(m--)
{
scanf("%d", &x);
if(w[x] > w[far]) far = x;
for(int i = x; !vis[i]; i = fa[i])
{
vis[i] = 1;
sum++;
}
if(spc) putchar('\n');
else spc = 1 ;
printf("%d", sum * 2 - w[far]);
}
return 0;
}
L2-4
当时第一个想法是用floyd算法,但是看到n最大为500的时候怕超时就用堆优化的迪杰斯特拉算法写了,结果还是超时了,应该是图比较稠密,导致堆优化效果不好,赛后队友说pta的机子比较快,floyd可以过,于是又重写了一遍。还有比赛的时候把最远距离搞反了,将自己到其他人的距离当成了最远距离,导致浪费了很多时间debug,实际上应该是别人到自己的最远距离。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#define mem(a, v) memset(a, v, sizeof(a))
#define inf 0x3f3f3f3f
const int N = 5e2 + 5;
using namespace std;
int g[N][N], dis[N][N], maxdis[N];
char sex[N];
int main()
{
mem(g, inf);
mem(dis, inf);
mem(maxdis, 0);
int n, m, x, d;
char c;
scanf("%d", &n);
getchar();
for(int i = 1; i <= n; i++)
{
sex[i] = getchar();
scanf("%d", &m);
while(m--)
{
scanf("%d%c%d", &x, &c, &d);
g[i][x] = d;
}
getchar();
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dis[i][j] = g[i][j];
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
if(sex[i] == sex[j]) continue;
maxdis[i] = max(maxdis[i], dis[j][i]);
}
}
int fmin = inf, mmin = inf;
vector<int> fv, mv;
for(int i = 1; i <= n; i++)
{
if(sex[i] != 'F') continue;
if(maxdis[i] < fmin)
{
fmin = maxdis[i];
fv.clear();
fv.push_back(i);
}
else if(maxdis[i] == fmin)
fv.push_back(i);
}
for(int i = 1; i <= n; i++)
{
if(sex[i] != 'M') continue;
if(maxdis[i] < mmin)
{
mmin = maxdis[i];
mv.clear();
mv.push_back(i);
}
else if(maxdis[i] == mmin)
mv.push_back(i);
}
bool spc = 0;
for(auto i : fv)
{
if(spc) putchar(' ');
else spc = 1;
printf("%d", i);
}
putchar('\n');
spc = 0;
for(auto i : mv)
{
if(spc) putchar(' ');
else spc = 1;
printf("%d", i);
}
return 0;
}
L3-1
咋一看有点麻烦,比赛的时候也是看了一会没什么思路,现在再看,是很明显的一道拓扑排序的题。在对序列进行比较的时候只要将两个长度一样的两个相邻序列进行比较即可。
我一开始在拓扑排序部分的做法是一次将所有入度为0的点都放到一个vector里,排序后输出,同时将这些点所连的那些点入度减一,下一轮再重复这种做法。但是提交的时候显示答案错误。后来想到了一个返例。
第一个输出的点是bbb,第二个点应该是abc,但是按照我的做法第二个点输出的是ccc,因此正确的做法是开一个优先队列,每输出一个点就更新剩下点的入度,并将入度为0的点入队。
AC代码:
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
#define inf 0x3f3f3f3f
const int N = 1e5 + 5;
using namespace std;
unordered_map<string, int> mp; //对单词进行编号
int idx = 0;
string pm[10005]; //记录编号对应的单词
unordered_map<int, bool> vis[N]; //记录这个单词和另一个单词之间是否连有边
vector<int> v[N];
int cnt[N]; //记录第i个序列有几个单词
int in[N]; //记录入度,出度越小的单词越小
string word[N][11];
struct node
{
int x;
};
bool operator<(node a, node b) { return pm[a.x] > pm[b.x]; }
int main()
{
mem(cnt, 0);
mem(in, 0);
int n;
char c;
scanf("%d", &n);
getchar();
for (int i = 1, j; i <= n; i++)
{
while (true)
{
for (j = 0;; j++)
{
c = getchar();
if (c != '.' && c != '\n')
word[i][cnt[i]] += c;
else
break;
}
if (mp[word[i][cnt[i]]] == 0)
{
mp[word[i][cnt[i]]] = ++idx;
pm[idx] = word[i][cnt[i]];
}
cnt[i]++;
if (c == '\n') break;
}
//两个输入序列的长度一样的时候,从左往右找到第一个不同的单词得到大小关系,
//后面的单词由于前面不同而无法比较,所以不用去管
if (i != 1 && cnt[i] == cnt[i - 1])
{
for (int j = 0;; j++)
{
if (word[i][j] != word[i - 1][j])
{
int id1 = mp[word[i - 1][j]], id2 = mp[word[i][j]];
if (vis[id1][id2] == 0)
{
vis[id1][id2] = 1;
v[id1].push_back(id2);
in[id2]++;
}
break;
}
}
}
}
bool spc = 0;
priority_queue<node> q;
for (int i = 1; i <= idx; i++)
if (in[i] == 0) q.push({i});
while (q.size())
{
if (spc)
putchar('.');
else
spc = 1;
node t = q.top();
printf("%s", pm[t.x].c_str());
q.pop();
for (auto it : v[t.x])
{
in[it]--;
if (in[it] == 0) q.push({it});
}
}
return 0;
}
L3-2
借用一下官方的题解:
考虑节点 u 与 v 满足 u>v,在多少 DFS 序中 u 能排在 v 前面。分以下几种情况:
- 若 u 是 v 的祖先,则所有 DFS 序中 u 都在 v 前面;
- 若 v 是 u 的祖先,则不存在这样的 DFS 序;
- 若 u 与 v 没有祖先关系,那么 u 排在 v 前面,意味着当搜索到 u 和 v 的 lca 时,先遍历了 u 所在的子树,后遍历了 v 所在的子树。遍历顺序可以看作是所有子节点的排列,很显然在一半的排列中,u 在 v 前面。
因此我们不妨先假设对于所有点对,都能在一半的 DFS 序中做出贡献。之后进行 DFS,遍历到节点 u 时,如果它的祖先比 u 大,那么我们少算了一半的 DFS 序;如果它的祖先比 u 小,那么我们多算了一半的 DFS 序。因此本题的本质是计算每个节点的祖先有多少比它大(小),用树状数组维护即可。复杂度 O(nlogn)。
另外只要一个节点枚举子节点的顺序不同,最终产生的 DFS 序就不同。因此 DFS 序的总数就是所有节点子节点数量阶乘相乘。
在提交过程中一直有一两个点过不去,索性将所有的int都改成了long long,没想到居然就过了…,只能说有取模的题目是真的玄学啊,下次保险起见都取long long好了QAQ
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 3e5 + 10;
const ll mod = 1e9 + 7;
ll fac[N];
void init()
{
fac[0] = 1;
for (ll i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % mod;
}
ll quick_power(ll base, ll power, ll mod)
{
ll result = 1;
base %= mod;
while (power)
{
if (power & 1)
result = (result * base) % mod;
power >>= 1;
base = (base * base) % mod;
}
return result;
}
//树状数组求逆序对
ll C[N], n;
ll lowbit(ll x)
{
return x & -x;
}
void add(ll x, ll val)
{
while (x <= n)
{
C[x] += val;
x += lowbit(x);
}
}
ll query(ll x)
{
ll res = 0;
while (x)
{
res += C[x];
x -= lowbit(x);
}
return res;
}
ll cnt1 = 0, cnt2 = 0, tot = 1;
vector<ll> v[N];
void dfs(ll x, ll fa)
{
tot = (tot * fac[v[x].size() - (fa != -1)]) % mod;
cnt1 = (cnt1 + query(n) - query(x)) % mod; //祖先里比当前大的数的个数
cnt2 = (cnt2 + query(x)) % mod; //祖先里比当前小的数的个数
add(x, 1);
for (ll to : v[x])
{
if (to == fa) continue;
dfs(to, x);
}
add(x, -1); //避免对其他非同祖先结点造成影响
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
init();
ll root, x, y;
cin >> n >> root;
for (ll i = 1; i < n; i++)
{
cin >> x >> y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(root, -1);
ll num = ((n * (n - 1) % mod * quick_power(2, mod - 2, mod) % mod - cnt1 - cnt2) % mod + mod) % mod; //没有父子关系的点对数量
ll ans = tot * (cnt1 + num * quick_power(2, mod - 2, mod) % mod) % mod; //加上有父子关系的逆序对数
cout << ans;
return 0;
}