A比较简单,随便做做,应该还有更简单的写法的,然后开始看B。B看了之后傻眼了去开C,把C过了之后回来看B,还是不会,就去开D,D好像蛮简单的。然后把D过了之后去看B,还是不会,看看dls7分钟过了F,去看了一下F,没搞清楚 (E[X^k]) 和 [((E[X])^k) 假了一会,回去看B,还是不会。但是好像看了下E,因为图论恐惧症导致E的恐怖值比B还高,硬着头皮看B。手动打表看出一点规律,简单猜想了一下就糊了一个算法上去。距离结束还有22分钟觉得不太有希望就走了(其实假如想清楚E不是图而是考dfn序的话还是有机会的,下次可能第一印象不会陷进图里面)。
假如正向开题然后B题不演的话,还是有机会5题的。
A - Shuffle Hashing
题意:给一个字符串s,和字符串t,问t中任意一段与s等长的字符串是否可以重排后变成s。
题解:枚举所有t中的字符串,然后全部sort暴力比较。
char s[1005], t[1005], r[1005];
void test_case() {
scanf("%s%s", s + 1, t + 1);
int sl = strlen(s + 1);
sort(s + 1, s + 1 + sl);
//cout << "s=" << s + 1 << endl;
int tl = strlen(t + 1);
r[sl + 1] = ' ';
for(int i = 1; i + sl - 1 <= tl; ++i) {
strncpy(r + 1, t + i, sl);
sort(r + 1, r + 1 + sl);
//cout << "r=" << r + 1 << endl;
if(strcmp(s + 1, r + 1) == 0) {
puts("YES");
return;
}
}
puts("NO");
}
但其实可以用双指针法,尺取出每一段的26个字母的数量,然后是26倍n就可以解决。
注意要给strncpy的字符串加上结束符,它自己不会加上,真蠢。所以要是可以的话还是用string实现吧。
string s, t;
void test_case() {
cin >> s >> t;
sort(s.begin(), s.end());
for(int i = 0; i < t.length(); ++i) {
string r = t.substr(i, s.length());
sort(r.begin(), r.end());
if(s == r) {
puts("YES");
return;
}
}
puts("NO");
return;
}
B - A and B
题意:给两个数A和B,第i次操作选择一个数+i,求最少的操作次数使得他们俩相等。
题解:
第一次操作:+1 -1
第二次操作:+3 +1 -1 -3
第三次操作:+6 +4 +2 0 -2 -4 -6
第四次操作:+10 +8 +6 +4 +2 0 -2 -4 -6 -8 -10
解释一下第四次操作:
+10:+1+2+3+4
+8:-1+2+3+4
+6:+1-2+3+4
+4:+1+2-3+4
+2:+1+2+3-4
0:-1+2+3-4
-2:+1-2+3-4
-4:+1+2-3-4
-6:-1+2-3-4
-8:+1-2-3-4
-10:-1-2-3-4
事实上每次把还没碰到右边的负数区的第一个负数右移一个位置,就会使得整体-2,假如没有这个数就把+1改成-1也是使得整体-2。所以可以构造出[-sum,+sum]里面和sum奇偶相同的数。
找出一堆数字的前缀和然后保证最大那个数字后面也有与它奇偶性相同的数出现就可以了。所以不妨搞个2e9。
ll ma[200005];
int mtop = 0;
void test_case_init() {
ll sum = 0;
for(int i = 1; sum <= 2ll * INF; ++i) {
sum += i;
ma[++mtop] = sum;
}
}
void test_case() {
ll a, b;
scanf("%lld%lld", &a, &b);
ll d = abs(a - b);
if(d == 0) {
puts("0");
return;
}
int p = lower_bound(ma + 1, ma + 1 + mtop, d) - (ma);
for(int i = p;; ++i) {
if((d - ma[i]) % 2 == 0) {
printf("%d
", i);
return;
}
}
}
C - Berry Jam
题意:给2n个罐子,分别是[1,2n],站在[n,n+1]的中间,吃掉包含这个点的左边连续一段和右边连续一段,使得剩下的罐子红色的和蓝色的数量相等。吃最少的罐子。
题解:把红色蓝色变成+1和-1,那么就把左边的前缀和插进map里面,然后右边的后缀和在map里面查询相反数,查到就更新。注意因为是吃得越少越好所以同一个值,map里存的应该是最靠右的前缀。
注意!要加上为0的前缀和和后缀和!
不过现场可以看出来并造样例验证,也是能力提高了一点。
4
1 1 1 1 1 2 1 2
这个PP可真良心。
int n;
int a[2000005];
int prefix[2000005];
int suffix[2000005];
map<int, int> M;
void test_case() {
scanf("%d", &n);
M.clear();
for(int i = 0; i <= 2 * n + 1; ++i) {
prefix[i] = 0;
suffix[i] = 0;
}
for(int i = 1; i <= 2 * n; ++i) {
scanf("%d", &a[i]);
if(a[i] == 2)
a[i] = -1;
}
for(int i = 1; i <= 2 * n; ++i)
prefix[i] = prefix[i - 1] + a[i];
for(int i = 2 * n; i >= 1; --i)
suffix[i] = suffix[i + 1] + a[i];
int ans = 0;
for(int i = 0; i <= n; ++i)
M[prefix[i]] = i;
for(int i = 2 * n + 1; i >= n + 1; --i) {
int t = -suffix[i];
if(M.count(t)) {
ans = max(ans, M[t] + 2 * n + 1 - i);
//cout << "i=" << M[t] << " j=" << 2 * n + 1 - i << endl;
}
}
printf("%d
", 2 * n - ans);
}
D - Segment Tree
题意:给2n个数字,每个数字[1,n]恰好出现2次,第一次称为l[i],第二次称为r[i]。任何两个部分交叉的线段代表的点之间连边,问连出来的是不是树。
题解:平衡树按r排序,遇到l就插进平衡树里面,那么每次就取出r<当前节点的r的点全部连一条边。当连的边不少于n条或者成环(实际上连的边有n条就一定会成环)就不可能是一棵树。当然,连的边不是n-1条也不会是一棵树。
好像对于这种有序性的还是平衡树方便又快,用堆还要反反复复取出来,常数小但是多个log。
struct DisjointSetUnion {
static const int MAXN = 1000005;
int n, fa[MAXN + 5], rnk[MAXN + 5];
void Init(int _n) {
n = _n;
for(int i = 1; i <= n; i++) {
fa[i] = i;
rnk[i] = 1;
}
}
int Find(int u) {
int r = fa[u];
while(fa[r] != r)
r = fa[r];
int t;
while(fa[u] != r) {
t = fa[u];
fa[u] = r;
u = t;
}
return r;
}
bool Merge(int u, int v) {
u = Find(u), v = Find(v);
if(u == v)
return false;
else {
if(rnk[u] < rnk[v])
swap(u, v);
fa[v] = u;
rnk[u] += rnk[v];
return true;
}
}
} dsu;
int n;
int l[1000005], r[1000005];
int a[1000005];
set<pii> PQ;
void test_case() {
scanf("%d", &n);
PQ.clear();
dsu.Init(2 * n);
a[0] = a[2 * n + 1] = 0;
for(int i = 1; i <= n; ++i) {
scanf("%d%d", &l[i], &r[i]);
a[l[i]] = i;
a[r[i]] = -i;
}
int cntedge = 0;
for(int i = 1; i <= 2 * n; ++i) {
if(a[i] > 0) {
int id = a[i];
//cout << "id=" << id << endl;
for(auto &e : PQ) {
if(r[id] < e.first)
break;
if(dsu.Merge(e.second, id)) {
//printf("%d - %d
", e.second, id);
++cntedge;
if(cntedge >= n) {
puts("NO");
return;
}
} else {
//printf("%d - %d but
", e.second, id);
puts("NO");
return;
}
}
PQ.insert({r[id], id});
} else {
PQ.erase(PQ.begin());
//int id = -a[i];
}
}
if(cntedge == n - 1)
puts("YES");
else
puts("NO");
}
E - Tests for problem D
题意:给一棵树,构造一个序列,使得D题可以复原这棵树。
题解:一开始看起来像广搜,但又不是广搜,又有点像深搜。在遍历一个点之后把它除父亲外所有的邻接点全部入栈然后把这个点的右边界弹出,就再也不会往后面连边了,而遍历这些邻接点的顺序就是先进先出,这样兄弟之间就是包含关系。注意!叶子节点是度数<=1的点!注意平凡的情况。
int root;
vector<int> G[500005];
void dfs1(int u, int p) {
if(G[u].size() <= 1) {
root = u;
return;
}
for(auto &v : G[u]) {
if(v == p)
continue;
dfs1(v, u);
if(root)
return;
}
}
int ans[1000005], cnt;
int pa[500005];
stack<int> st;
void dfs(int u) {
for(auto &v : G[u]) {
if(v == pa[u])
continue;
ans[++cnt] = v;
pa[v] = u;
st.push(v);
}
}
int l[500005], r[500005];
void test_case() {
int n;
scanf("%d", &n);
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);
}
dfs1(1, -1);
ans[++cnt] = root;
st.push(root);
pa[root] = -1;
while(st.size()) {
int u = st.top();
st.pop();
dfs(u);
ans[++cnt] = u;
}
for(int i = 1; i <= cnt; ++i) {
int t = ans[i];
if(l[t] == 0)
l[t] = i;
else
r[t] = i;
//printf("%d%c", ans[i], "
"[i == cnt]);
}
for(int i = 1; i <= n; ++i)
printf("%d %d
", l[i], r[i]);
}
但是仔细一想,这个树上嵌套序列不是很像dfn序吗?只是父亲不再是嵌套子树了,而是和子树部分交叉,所以根据dfs来改是很自然的。下次不要怕。
发现其实使用stack来反转的是多个子节点进入答案的顺序,其实可以直接reverse。而且根是不是叶子是无关紧要的。经过一个这样的反转,两棵子树之间一定就是嵌套关系,而父子之间就会交错。所以说这种是不是可以起个名字叫做反dfn序?
vector<int> G[500005];
int l[500005], r[500005], cnt;
void dfs(int u, int p) {
for(auto &v : G[u]) {
if(v == p)
continue;
l[v] = ++cnt;
}
reverse(G[u].begin(), G[u].end());
r[u] = ++cnt;
for(auto &v : G[u]) {
if(v == p)
continue;
dfs(v, u);
}
}
void test_case() {
int n;
scanf("%d", &n);
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);
}
cnt = 0;
l[1] = ++cnt;
dfs(1, -1);
for(int i = 1; i <= n; ++i)
printf("%d %d
", l[i], r[i]);
}
好像得益于reverse的超大常数这个跑得比上面的还慢。