7.18 海高集训 杂题选讲
出/搬题人:\(\text{D}\color{red}\text{eaphetS}\)
#A. [NOI Online #1 入门组] 跑步
这题相当于我们求任意模数的拆分数,可以使用五边形数定理。
五边形数定理用来描述欧拉函数展开式的特性:
\(φ(x) = \prod^{∞}_{n = 1}(1 - x^n) = \sum_{k = -∞}^{∞}(-1)^kx^{\frac {k(3k + 1)} {2}} = \sum_{k = 0}^{∞}(-1)^kx^{\frac {k(3k ± 1)} {2}}\)
欧拉函数的倒数是划分数的生成函数:
\(\frac {1} {φ(x)} = \sum_{i = 0}^{∞}p(i)x^i = \prod^{∞}_{i = 0}\sum_{j = 0}^{∞}x_{ij} = \prod^{∞}_{i = 0}\frac {1} {1 - x^i}\)
由定义我们可以得到:
\((\sum_{k = 0}^{∞}(-1)^kx^{\frac {k(3k ± 1)} {2}})(\sum_{i = 0}^{∞}p(i)x^i) = 1\)
通过这个我们可以得到划分数的递推式。
同时发现五边形数生成函数的项数是 \(O(\sqrt n)\) 级别的,故我们可以在 \(O(n \sqrt n)\) 的时间复杂度内求出划分数前 \(n\) 项。
#include<bits/stdc++.h>
using namespace std;
long long T, n, m, mod, f[100010], g[100010];
int main()
{
f[0] = 1;
scanf("%lld %lld", &n, &mod);
for (long long i = 1; i * (3 * i - 1) / 2 <= n; i++)
{
g[m++] = i * (3 * i - 1) / 2;
g[m++] = i * (3 * i + 1) / 2;
}
for (long long i = 1; i <= n; i++)
for (long long j = 0; j < m && g[j] <= i; j++)
f[i] = (f[i] + (((j >> 1) & 1) ? -1 : 1) * f[i - g[j]]) % mod;
printf("%lld", (f[n] + mod) % mod);
return 0;
}
#B. You Are Given a Tree
基本思路,我们可以有最基本的想法,就是贪心,我们从每棵子树底遍历。
遇到能接上后长度大于 \(k\) 的,就接上,至于正确性……可以自己手玩,发现是正确的……贪心需要证明还叫贪心吗??
可惜啊可惜,一次贪心是 \(O(n)\) 的,你求 \(1...n\) 就是 \(O(n^2)\) 的好算法。
我们发现,随着 \(k\) 的增大,答案啊,是一直变小的。
还可以发现,在 \(k>\sqrt n\),时,答案的取值也只有 \(\sqrt n\) 种。
对于 \(k≤\sqrt n\),直接来,时间复杂度是 \(O(n\sqrt n)\) 的。
剩下的部分,我们可以二分有哪些值是相同的,最多也只需要 \(\sqrt n\) 次,时间复杂度为 \(O(\frac {n^2 \log n} {\sqrt n})\)。
总的时间复杂度就是 \(O(n \sqrt n+\frac {n^2 \log n} {\sqrt n})\),但是但是,这样不是最优的。
我们设分治的点是 \(T\),时间复杂度为 \(O(nT+\frac {n^2 \log n} {T})\)
对于这两项,他们乘起来是定值,可以用基本不等式来搞,时间复杂度最小为 \(2 \times \sqrt{n^3 \log n}\)。
也就是 \(nT = \frac {n^2 \log n} {T}\) 时,可解得 \(T = \sqrt {n \log n}\),这算个根号分治的小优化吧。
#include <bits/stdc++.h>
using namespace std;
long long nxt[200020], to[200020], head[100010], tot, tim, n, fa[100010], dfn[100010], ans[100010], f[100010];
inline void addedge(long long u, long long v)
{
nxt[++tot] = head[u];
to[tot] = v;
head[u] = tot;
}
inline void dfs(long long u, long long pre)
{
fa[u] = pre;
for (long long i = head[u]; i; i = nxt[i])
{
long long v = to[i];
if (v == pre)
continue;
dfs(v, u);
}
dfn[++ tim] = u;
}
inline long long solve(long long k)
{
long long res = 0;
for (long long i = 1; i <= n; ++ i)
f[i] = 1;
for (long long i = 1; i <= n; ++ i)
{
long long u = dfn[i], fau = fa[u];
if (fau && f[u] != -1 && f[fau] != -1)
if (f[u] + f[fau] >= k)
{
res ++;
f[fau] = -1;
}
else
f[fau] = max(f[fau], f[u] + 1);
}
return res;
}
int main()
{
scanf("%lld", &n);
long long p = sqrt(n * log2(n));
for (long long i = 1, u, v; i < n; ++i)
{
scanf("%lld %lld", &u, &v);
addedge(u, v);
addedge(v, u);
}
dfs(1, 0);
ans[1] = n;
for (long long i = 2 ; i <= p ; ++ i)
ans[i] = solve(i);
for (long long i = p + 1; i <= n; ++i)
{
long long tmp = solve(i), l = i, r = n, res = i;
while (l < r - 1)
{
long long mid = l + r >> 1;
if (solve(mid) == tmp)
{
l = mid;
res = max(res, mid);
}
else r = mid;
}
for (; i <= res; ++i)
ans[i] = tmp;
--i;
}
for (long long i = 1; i <= n; ++ i)
printf("%lld\n", ans[i]);
return 0;
}
#C. 植树(plant)
在 \(n,m≤10^5\) 的数据范围下, \(O(\frac {m^2} {n} log^2m)\)的复杂度在 \(n\) 较小时就炸掉了,于是考虑针对 \(n\) 的大小进行数据分块
注意到,如果我们直接往里面加边并动态维护最小生成树,直到形成一个满足条件的生成树后把图清空。这样的做法是 \(O(nm)\) 的
至于如何动态维护最小生成树,可以在加边时直接暴力求出 \(x,y\) 到 \(LCA\) 上的边权最大值,然后判断是否更新,每次更新也可以 \(O(n)\) 进行
那么可以发现,\(min(\frac {m^2} {n} log^2m,nm)≈m \sqrt {mlogm}\),卡一卡就过去了
#include <bits/stdc++.h>
using namespace std;
struct Node
{
long long x, y, w;
}Edge[100010];
long long T, n, m, s, fa[100010];
inline long long Find(long long x)
{
return fa[x] ? fa[x] = Find(fa[x]) : x;
}
inline bool check(long long l, long long r)
{
for(long long i = 1; i <= n; ++i)
fa[i] = 0;
vector<Node> v;
for(long long i = l; i <= r; ++i)
v.push_back(Edge[i]);
sort(v.begin(), v.end(), [](Node a, Node b) {return a.w < b.w;});
long long sum = 0, cnt = 0;
for(auto e : v)
{
long long fax = Find(e.x), fay = Find(e.y);
if(fax != fay)
{
++cnt;
sum += e.w;
fa[fay] = fax;
}
}
return cnt == n - 1 && sum <= s;
}
int main()
{
scanf("%lld", &T);
while(T--)
{
scanf("%lld %lld %lld", &n, &m, &s);
for(long long i = 1; i <= m; ++i)
scanf("%lld %lld %lld", &Edge[i].x, &Edge[i].y, &Edge[i].w);
long long ans = 0;
for(long long i = 1, j; i <= m; i = j + 1)
{
long long len = 1;
while(i + len - 1 <= m && !check(i, min(i + len - 1, m)))
len *= 2;
if(i + len - 1 > m && !check(i, m))
break;
long long l = i + len / 2, r = min(i + len - 1, m);
while(l <= r)
{
long long mid = l + r >> 1;
if(check(i, mid))
{
r = mid - 1;
j = mid;
}
else l = mid + 1;
}
++ans;
}
printf("%lld\n", ans);
}
return 0;
}
#D. [AHOI2014/JSOI2014] 拼图
记 \(M=∑W_i\)。
注意到数据范围中 \(N×M≤10^5\),这提示我们可以分两种情况进行。
在讨论之前,先思考一下:如果我们已知矩形的上下边界,如何在 \(O(M)\) 的时间里得到答案。
显然,最终答案矩形一定是横跨若干个矩形,然后是左右两端的矩形的一部分(可能没有)。不妨设上下边界分别是 \(l,r\)。那么我们先找出在 \([l,r]\) 全是 \(0\) 的矩形,这些矩形肯定是放中间的。然后再去寻找左右边界。可以记录每个矩形从左或右开始在 \([l,r]\) 中最多有几列,并把其中的最大值所对应的矩形放在左右端点。但是可能某一矩形被同时放在左右端点。为了防止这种情况,需要再统计一个次大值。这个复杂度是 \(O(M)\)。
回到上面的讨论。若 \(N≤M\),那么 \(N≤\sqrt {10^5}\),我们就可以直接枚举上下边界,用上文的做法即可,总复杂度 \(O(N^2M)\)。反之,那我们统计出每个点上方的第一个 \(1\) 的位置,记为 \(up_{i,j}\),然后以 \([up_{i,j},i]\) 作为左右边界,同样使用上文的做法即可。总复杂度 \(O(NM^2)\)。
另外,由于每个矩形的大小不能保证一个较小的范围,所以直接存会爆空间,因此我们需要二维转一维存图。
#include <bits/stdc++.h>
using namespace std;
long long T, num, n, m, ans, w[100010], st[100010], ed[100010], a[100010], sum[100010], up[100010];
char ch[100010];
inline long long id(long long x, long long y)
{
if (x < 1 || y < 1)
return 0;
return (y - 1) * n + x;
}
inline long long getsum(long long xx1, long long yy1, long long xx2, long long yy2)
{
return sum[id(xx2, yy2)] - sum[id(xx1 - 1, yy2)] - sum[id(xx2, yy1 - 1)] + sum[id(xx1 - 1, yy1 - 1)];
}
inline void calc(long long l, long long r)
{
if (l > r)
return;
for (long long i = 1, res = 0; i <= num; ++i)
for (long long j = st[i]; j <= ed[i]; ++j)
{
if (getsum(l, j, r, j) == 0)
++res;
else res = 0;
ans = max(ans, res * (r - l + 1));
}
long long lmx[2][2], rmx[2][2], len = 0;
memset(lmx, 0, sizeof(lmx));
memset(rmx, 0, sizeof(rmx));
for (long long i = 1; i <= num; ++i)
{
long long llen = 0, rlen = 0;
for (long long j = st[i]; j <= ed[i]; ++j)
if (getsum(l, j, r, j) == 0)
llen++;
else break;
for (long long j = ed[i]; j >= st[i]; --j)
if (getsum(l, j, r, j) == 0)
rlen++;
else break;
if (llen == w[i])
len += w[i];
else
{
if (llen > lmx[0][0])
{
lmx[1][0] = lmx[0][0];
lmx[1][1] = lmx[0][1];
lmx[0][0] = llen;
lmx[0][1] = i;
}
else if (llen > lmx[1][0])
{
lmx[1][0] = llen;
lmx[1][1] = i;
}
if (rlen > rmx[0][0])
{
rmx[1][0] = rmx[0][0];
rmx[1][1] = rmx[0][1];
rmx[0][0] = rlen;
rmx[0][1] = i;
}
else if (rlen > rmx[1][0])
{
rmx[1][0] = rlen;
rmx[1][1] = i;
}
}
}
ans = max(ans, (r - l + 1) * len);
for (long long i = 0; i < 2; ++i)
for (long long j = 0; j < 2; ++j)
if (lmx[i][1] != rmx[j][1])
ans = max(ans, (r - l + 1) * (len + lmx[i][0] + rmx[i][0]));
}
int main()
{
scanf("%lld", &T);
while (T--)
{
m = ans = 0;
memset(sum, 0, sizeof(sum));
scanf("%lld %lld", &num, &n);
for (long long i = 1; i <= num; ++i)
{
scanf("%lld", &w[i]);
st[i] = m + 1;
for (long long j = 1; j <= n; ++j)
{
scanf("%s", ch + 1);
for (long long k = 1; k <= w[i]; ++k)
a[id(j, m + k)] = ch[k] - '0';
}
m += w[i];
ed[i] = m;
}
for (long long i = 1; i <= n; ++i)
for (long long j = 1; j <= m; ++j)
sum[id(i, j)] = sum[id(i, j - 1)] + sum[id(i - 1, j)] - sum[id(i - 1, j - 1)] + a[id(i, j)];
for (long long j = 1; j <= m; ++j)
for (long long i = 1; i <= n; ++i)
if (a[id(i, j)] == 1)
up[id(i, j)] = i;
else up[id(i, j)] = up[id(i - 1, j)];
if (n <= m)
for (long long i = 1; i <= n; ++i)
for (long long j = i; j <= n; ++j)
calc(i, j);
else for (long long i = 1; i <= n; ++i)
for (long long j = 1; j <= m; ++j)
calc(up[id(i, j)] + 1, i);
printf("%lld\n", ans);
}
return 0;
}
#E. [NOI Online #3 提高组] 魔法值
首先,咱们来看看题目里面说的魔法值是咋定义的:
\(f_{x,i}=f_{v_1,i−1}⊕f_{v_2,i−1}⊕⋯⊕f_{v_k,i−1}\)
那有了邻接矩阵 \(e\) 之后, 有边为 \(1\),没边为 \(0\) ,我们的魔法值就可以这么定义啦!
\(f_{x,i}=f_{1,i−1}×e_{1,x}⊕f_{2,i−1}×e_{2,x}⊕⋯⊕f_{n,i−1}×e_{n,x}\)
就类似于这样:
\(c_{i,j}=a_{i,1}×b_{1,j}+a_{i,2}×b_{2,j}+⋯+a_{i,n}×b_{n,j}=∑_{k=1}^na_{i,k}×b_{k,j}\)
这样的话,我们可以重新定义两个矩阵 \(a\) 和 \(b\) 的乘法为 \(a_{i,k}×b_{k,j}\) 的异或和:
\(a×b=a_{i,1}×b_{1,j}⊕a_{i,2}×b_{2,j}⊕⋯⊕a_{i,n}×b_{n,j}\)
为了让这个“异或版矩阵乘法”的定义用得更顺手,咱们干脆把 \(f_{i,j}\) 的定义改一下,\(i\)为天数,\(j\) 为城市编号 ,即 \(f_{i,j}\) 为第 \(i\) 天,城市 \(j\) 的魔法值。
这样一来,根据“异或版矩阵乘法”的定义,新一天各个城市的魔法值(它是一个 \(1\) 行 \(n\) 列的矩阵)就可以通过前一天的魔法值与图的邻接矩阵通过“异或版矩阵乘法”的运算得到。
#include <bits/stdc++.h>
using namespace std;
struct matrix
{
long long Mat[110][110], x, y;
matrix()
{
x = 0;
y = 0;
memset(Mat, 0, sizeof(Mat));
};
}M[40];
matrix operator * (const matrix &a, const matrix &b)
{
matrix c;
c.x = a.x, c.y = b.y;
for (long long i = 1; i <= a.x; i++)
for (long long j = 1; j <= b.y; j++)
for (long long k = 1; k <= a.y; k++)
c.Mat[i][j] ^= a.Mat[i][k] * b.Mat[k][j];
return c;
}
long long f[110], n, m, q;
int main()
{
scanf("%lld %lld %lld", &n, &m, &q);
for (long long i = 1; i <= n; i++)
scanf("%lld", &f[i]);
M[0].x = n;
M[0].y = n;
for (long long i = 1, a, b; i <= m; i++)
{
scanf("%lld %lld", &a, &b);
M[0].Mat[a][b] = M[0].Mat[b][a] = 1;
}
for (long long i = 1; i < 32; i++)
M[i] = M[i - 1] * M[i - 1];
while (q--)
{
long long x;
scanf("%lld", &x);
matrix ans;
for (long long i = 1; i <= n; i++)
ans.Mat[1][i] = f[i];
ans.x = 1;
ans.y = n;
for (long long i = 0; i < 32; i++)
if ((x >> i) & 1)
ans = ans * M[i];
printf("%lld\n", ans.Mat[1][1]);
}
return 0;
}
#F. [NOI Online #1 入门组] 魔法
我们来根据 \(k\) 的情况分类讨论下:
- \(k=0\)
非常 trivial 的情况,直接跑 Floyd 即可得出结果。
- \(k=1\)
考虑从 \(k=0\) 的情况推 \(k=1\) 的情况。
设 \(f_{k,i,j}\) 表示在使用不超过 \(k\) 次魔法的情况下,从 \(i\) 到 \(j\) 的最短路。
现在我们知道了 \(f_{0,i,j}\),如何得到 \(f_{1,i,j}\) 呢?
边的规模只有 \(2500\),我们可以直接枚举要用魔法的边,转移时强制走这条边,求最短路。
假如边 \((u,v,w)\) 用了魔法,转移方程如下:
\(f_{1,i,j}=min\{f_{0,i,j},minf_{0,i,u}+f_{0,v,j}−w\}\)
- \(k=2\)
我们已经得到了 \(k=1\) 的情况,如何推 \(k=2\) 的情况呢?
类似于 Floyd,我们可以枚举一个中转点 \(k\),\(i→k\) 最多用一次魔法,\(k→j\) 最多用一次魔法,合并起来就是最多两次魔法的答案了。
写成式子长这样:
\(f_{2,i,j}=minf_{1,i,k}+f_{1,k,j}\)
如果你做过很多矩阵优化的题,会发现这个形式挺像矩阵乘法。
所不同的是:原来是若干个乘积的和相加,现在变成了若干个和取最小值。
类比一下,我们猜想这个可以用矩阵快速幂来优化转移。
- \(k>2\)
根据 \(k=2\) 的情况,我们猜想:最多用 \(i\) 次魔法的结果和最多用 \(j\) 次魔法的结果按照上面的转移方式合并的话,可以得到最多用 \(i+j\) 次魔法的结果。
这个成立的前提是我们定义的这个矩阵运算要具有结合律,这样我们才能用快速幂来计算。
数的加法和乘法都满足结合律,从而有龟速乘和快速幂。
矩阵的乘法满足结合律,从而能用矩阵快速幂加快计算速度。
我们这个运算满足结合律吗?答案是肯定的。
到这里本题就得到了完美解决。
#include <bits/stdc++.h>
using namespace std;
struct Node
{
long long u, v, w;
}Edge[2505];
long long n, m, k, f[110][110];
struct Matrix
{
long long a[110][110];
Matrix(long long x = 63) {memset(a, x, sizeof(a));}
Matrix operator * (const Matrix &b) const
{
Matrix ans;
for (long long k = 1; k <= n; k++)
for (long long i = 1; i <= n; i++)
for (long long j = 1; j <= n; j++)
ans.a[i][j] = min(ans.a[i][j], a[i][k] + b.a[k][j]);
return ans;
}
}a;
Matrix ksm(Matrix x, long long y)
{
Matrix ans;
for (long long i = 1; i <= n; i++)
for (long long j = 1; j <= n; j++)
ans.a[i][j] = f[i][j];
while (y)
{
if (y & 1)
ans = ans * x;
x = x * x;
y >>= 1;
}
return ans;
}
int main()
{
memset(f, 63, sizeof(f));
scanf("%lld %lld %lld", &n, &m, &k);
for (long long i = 1; i <= n; i++)
f[i][i] = 0;
for (long long i = 1; i <= m; i++)
{
scanf("%lld %lld %lld", &Edge[i].u, &Edge[i].v, &Edge[i].w);
f[Edge[i].u][Edge[i].v] = Edge[i].w;
}
for (long long k = 1; k <= n; k++)
for (long long i = 1; i <= n; i++)
for (long long j = 1; j <= n; j++)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
for (long long k = 1; k <= m; k++)
{
long long u = Edge[k].u, v = Edge[k].v, w = Edge[k].w;
for (long long i = 1; i <= n; i++)
for (long long j = 1; j <= n; j++)
a.a[i][j] = min(a.a[i][j], min(f[i][j], f[i][u] + f[v][j] - w));
}
if (!k)
printf("%lld", f[1][n]);
else printf("%lld", ksm(a, k).a[1][n]);
return 0;
}
#G. Madhouse (Easy version)
#H. Madhouse (Hard version)
考虑询问\(s[1..n]\)和\(s[2..n]\),共计返回字符串数为\(\frac{n*(n+1)}{2}+\frac{n*(n-1)}{2}=n^2\),询问次数为2,满足要求。
相对于\(s[2..n]\),询问\(s[1..n]\)得到的子串额外增加了\(s[1..i](1 \leq i \leq n)\),如果我们能够得到这\(n\)个字符串,那么可以通过长度区分出他们。\(s[1..i]\)比\(s[1..i-1]\)多出来的那一个字符,便是字符串第\(i\)位上的字符。
使用\(\text{sort}\)和\(\text{multiset}\)实现去重,首先将读入的每个字符串都进行一遍排序,这样两个由同样字符组成(顺序可以不同)的字符串会得到同一个结果。本题中因为子串被打乱,顺序并不重要。
将\(s[1..n]\)询问的结果插入\(\text{multiset}\),再把\(s[2..n]\)询问的结果从\(\text{multiset}\)中删掉,那么最后剩下来的\(n\)个字符串就是我们需要的结果。依此可以复原出原串。
#include <bits/stdc++.h>
using namespace std;
string s, ss[110];
multiset<string> st;
char ans[1000];
inline bool cmp(string s1, string s2)
{
return s1.size() < s2.size();
}
int buc[1100], n;
int main()
{
cin >> n;
if (n == 1)
{
cout << "? " << 1 << ' ' << 1 << '\n';
fflush(stdout);
char c;
cin >> c;
cout << "! " << c;
fflush(stdout);
return 0;
}
cout << "? " << 1 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n + 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.insert(s);
}
cout << "? " << 2 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n - 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.erase(st.find(s));
}
int cnt = 0;
for (auto p : st)
ss[cnt++] = p;
sort(ss, ss + cnt, cmp);
ans[0] = ss[0][0];
for (int i = 1; i < cnt; i++)
{
for (auto p : ss[i - 1])
buc[p]--;
for (auto p : ss[i])
buc[p]++;
for (int j = 0; j < 255; j++)
if (buc[j] > 0)
{
buc[j] = 0;
ans[i] = j;
}
}
cout << "! " << ans;
fflush(stdout);
return 0;
}
困难版问题对返回字符串数的限制下调至\(\lceil 0.777(n+1)^2 \rceil\),使得原本的解法仅在\(n\)很小的时候直接适用。
不过我们仍然可以利用询问\(s[1...n_1],s[2...n_1]\)的解法,花费\(n_1^2\)的代价,求出\(s[1..n_1]\)。取\(n_1=(n+1)/2\),接下来,我们询问一次\(s[1..n]\)。总返回字符串数不超过\(0.75*(n+1)^2\),满足要求。
假设我们已经求出了\(s[n-k+1...n](n-k > n_1)\)位置的字符,接下来将要求出\(s[n-k]\)位置的字符。考虑\(s\)的长度为\(n-k-1\)的子串:
我们发现:
\(s[i](1\leq i \leq k+1)\)只被\(i\)个子串所包含;
\(s[i](n-k \leq i \leq n)\)只被\(n+1-i\)个子串所包含;
\(s[i](k+2 \leq i \leq n-k-1)\)被所有\(k+2\)个子串所包含;
而\(s[i] (1\leq i \leq k+1~or~(n-k+1 \leq i \leq n))\)我们是已知的。
考虑开一个桶,统计询问所得到的所有长度为\(n-k-1\)的字符串中,每个字符的出现次数,然后减去\(s[i] (1\leq i \leq k+1~or~(n-k+1 \leq i \leq n))\)这些已知位置的字符所带来的贡献。
最终桶里只剩下\(s[i](k+2 \leq i \leq n-k)\)这些位置带来的贡献。
鉴于对于\(s[k+2...n-k-1]\)位置的字符,每个字符贡献出现次数为\(k+2\),而唯独\(s[n-k]\),出现次数是\(n+1-(n-k)=k+1\)。
将桶中的出现次数对\(k+2\)取模,不为\(0\)的那个字符即是\(s[n-k]\)的值。
重复以上步骤,我们可以从后向前一路求出\(s\)。
#include <bits/stdc++.h>
using namespace std;
string s, ss[110];
multiset<string> st;
vector<string> v[110];
char ans[1000];
inline bool cmp(string s1, string s2)
{
return s1.size() < s2.size();
}
int buc[1100], n;
inline void branch()
{
int n1 = n;
n = (n + 1) / 2;
cout << "? " << 1 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n + 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.insert(s);
}
cout << "? " << 2 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n - 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.erase(st.find(s));
}
int cnt = 0;
for (auto p : st)
ss[cnt++] = p;
sort(ss, ss + cnt, cmp);
ans[0] = ss[0][0];
for (int i = 1; i < cnt; i++)
{
for (auto p : ss[i - 1])
buc[p]--;
for (auto p : ss[i])
buc[p]++;
for (int j = 0; j < 255; j++)
if (buc[j] > 0)
{
buc[j] = 0;
ans[i] = j;
}
}
cout << "? " << 1 << ' ' << n1 << '\n';
fflush(stdout);
for (int i = 0; i < n1 * (n1 + 1) / 2; i++)
{
cin >> s;
v[s.size()].push_back(s); //按长度分类
}
for (int i = n1 - 1; i >= n; i--)
{
for (int j = 'a'; j <= 'z'; j++)
buc[j] = 0;
for (auto p : v[i])
for (auto q : p)
buc[q]++; //统计出现次数
for (int j = 0; j < (n1 - i); j++)
buc[ans[j]] -= (j + 1);
for (int j = n1 - 1; j > i; j--)
buc[ans[j]] -= n1 - j; //消除贡献
for (int j = 'a'; j <= 'z'; j++)
if (buc[j] % (n1 + 1 - i) != 0)
ans[i] = j;
}
cout << "! ";
for (int i = 0; i < n1; i++)
cout << ans[i];
fflush(stdout);
exit(0);
}
int main()
{
cin >> n;
if (n == 1)
{
cout << "? " << 1 << ' ' << 1 << '\n';
fflush(stdout);
char c;
cin >> c;
cout << "! " << c;
fflush(stdout);
return 0;
}
if (n >= 6)
branch();
cout << "? " << 1 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n + 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.insert(s);
}
cout << "? " << 2 << ' ' << n << '\n';
fflush(stdout);
for (int i = 0; i < n * (n - 1) / 2; i++)
{
cin >> s;
sort(s.begin(), s.end());
st.erase(st.find(s));
}
int cnt = 0;
for (auto p : st)
ss[cnt++] = p;
sort(ss, ss + cnt, cmp);
ans[0] = ss[0][0];
for (int i = 1; i < cnt; i++)
{
for (auto p : ss[i - 1])
buc[p]--;
for (auto p : ss[i])
buc[p]++;
for (int j = 0; j < 255; j++)
if (buc[j] > 0)
{
buc[j] = 0;
ans[i] = j;
}
}
cout << "! " << ans;
fflush(stdout);
return 0;
}
#I. 无向图三元环计数
我们考虑给所有的边一个方向。具体的,如果一条边两个端点的度数不一样,则度数较小的点连向度数较大的点,否则由编号较小的点连向编号较大的点。不难发现这样的图是有向无环的。注意到原图中的三元环一定与对应有向图中所有形如 \(<u→v>,<u→w>,<v→w>\) 的子图一一对应,我们只需要枚举 \(u\) 的出边,再枚举 \(v\) 的出边,然后检查 \(w\) 是不是 \(u\) 指向的点即可。
时间复杂度是 \(O(m \sqrt m)\)。
#include <bits/stdc++.h>
using namespace std;
long long n, m, ans, deg[200020], A[200020], B[200020], vis[200020];
vector<long long> G[200020];
int main()
{
scanf("%lld %lld", &n, &m);
for (long long i = 1; i <= m; ++i)
{
scanf("%lld %lld", &A[i], &B[i]);
++deg[A[i]];
++deg[B[i]];
}
for (long long u, v; m; --m)
{
u = A[m];
v = B[m];
if (deg[u] > deg[v])
swap(u, v);
else if ((deg[u] == deg[v]) && (u > v))
swap(u, v);
G[u].push_back(v);
}
for (long long u = 1; u <= n; ++u)
{
for (auto v : G[u])
vis[v] = u;
for (auto v : G[u])
for (auto w : G[v])
if (vis[w] == u)
++ans;
}
printf("%lld", ans);
return 0;
}
#J. [USACO19DEC] Meetings S
首先我们不考虑重量,于是我们容易发现可以把相遇变成直接穿过。
我们又发现牛相遇会掉头,所以牛的相对位置不变(即最开始是左边第\(k\)只牛,之后肯定一直是左边第\(k\)只牛)。
我们又可以将牛的相对位置对应到它的重量。
这时候,我们就可以用以下的方法来计算出整个过程结束的时间\(T\):
- 我们把每只牛看作一直沿着初始的方向走,计算它们走到牛棚的时间\(t_i\)与进入的牛棚\(h_i\)。由于我们可以看作直接穿过,所以当且仅当在每个\(t_i\)时刻有一只牛进入了牛棚\(h_i\)。
- 然后我们可以将下标按\(t_i\)从小到大排序,然后按\(t_i\)升序扫描,扫到一个\(t_i\)时看\(h_i\),因为牛的相对位置不变,所以最左边的牛没有进入\(0\)处的牛棚时左边第二只牛不可能进入\(0\)处的牛棚。于是我们对于\(h_i\)为\(0\)或\(L\)处的牛棚依次在进入牛棚的牛重量和中加上当前没有进过牛棚的最左边或最右边的牛的重量,然后把这只牛标记为已经进入牛棚(相当于一个双端队列的队头或队尾出队)。
- 当某一只牛的重量被加上后进入牛棚的牛总重量超过全部牛重量和的一半时,\(T\)就是当前这只牛进入牛棚的时间\(t_i\)。
计算出\(T\),我们就可以计算牛的相遇次数了。
我们首先知道对于每只牛,由于我们可以看作相遇直接穿过,所以我们对于每只牛直接让它走\(T\)时间,最后到达的位置上肯定有牛。
我们会发现如果这样的话每只牛初始和结束时的相对位置会改变。我们知道,如果一只牛的相对位置从\(i\)变成\(j\),那么原来相对位置在\(i\)至\(j\)之间除这只牛以外的牛必须都与它相遇了,它才可能完成相对位置从\(i\)到\(j\)的过程。
我们视线追踪每只牛,看它们直接走的过程中相遇,那么在整个过程的进行中肯定有一次相遇发生在这个时间这个地点。
于是我们对于每只牛将它们的最终位置进行排序,然后将排序后的下标与初始下标取差的绝对值,求和,又因为一次相遇有两只牛,我们追踪每只牛时计算了两次,于是我们将得到的和除以2就得到了答案。
但是在牛棚的相遇不算相遇,而如果时刻\(T\)有牛在同一点则算相遇,我们需要处理这个。
其实我们只要在std::sort
的cmp
函数里对牛棚和非牛棚“区别对待”,对于位置相等且是牛棚就按初始的相对位置升序排序,不在就按降序排序,只要这样,我们就可以保证牛到了牛棚里不被算作是相遇,不在牛棚里被算作是相遇了。
#include <bits/stdc++.h>
using namespace std;
int L, N, T, W;
struct cow
{
int tim, hos;
} c[50050];
inline bool cmp(cow a, cow b)
{
return a.tim <= b.tim;
}
struct COW
{
int id, tmp;
} C[50050];
inline bool CMP(COW A, COW B)
{
if (A.tmp == B.tmp)
return (A.tmp == L || A.tmp == 0) ? A.id<B.id: A.id>B.id;
return A.tmp < B.tmp;
}
struct Cow
{
int wh, st, dr;
} Cw[50050];
inline bool Cmp(Cow a, Cow b)
{
return a.st < b.st;
}
int w, x, d;
int main()
{
scanf("%d %d", &N, &L);
W = T = 0;
int wt = 0;
long long ans = 0;
for (int i = 0; i < N; ++i)
{
scanf("%d %d %d", &w, &x, &d);
W += w;
Cw[i].st = x;
Cw[i].dr = d;
Cw[i].wh = w;
}
sort(Cw, Cw + N, Cmp);
for (int i = 0; i < N; ++i)
{
c[i].tim = (Cw[i].dr > 0) ? L - Cw[i].st : Cw[i].st;
c[i].hos = (Cw[i].dr > 0) ? 1 : 0;
}
sort(c, c + N, cmp);
int min = 0, max = N - 1;
for (int i = 0; i < N; ++i)
{
wt += Cw[(c[i].hos) ? max-- : min++].wh;
if ((wt << 1) >= W)
{
T = c[i].tim;
break;
}
}
for (int i = 0; i < N; ++i)
{
C[i].id = i;
C[i].tmp = (Cw[i].dr == 1) ? ((Cw[i].st + T > L) ? L : Cw[i].st + T) : ((Cw[i].st < T) ? 0 : Cw[i].st - T);
}
sort(C, C + N, CMP);
for (int i = 0; i < N; ++i)
ans += (C[i].id > i) ? C[i].id - i : i - C[i].id;
printf("%lld\n", ans / 2);
return 0;
}
#K. Tests for problem D
这道题给了一棵 \(n\) 个点的树,要求我们构造 \(n\) 个区间,使得对于任意两个区间 \(i,j\),如果它们之间相交但是没有包含关系,则 \(i,j\) 之间有边;反之则无边。
显然,对于一个点 \(u\),取任意两个与之相连的点 \(v_1,v_2\),则第 \(v_1\) 个区间和第 \(v_2\) 个区间一定不相交或者包含。
不妨我们只让一个邻接的点在左边与 \(u\) 相交,剩下的都在右边和 \(u\) 相交,并互相包含。
于是我们整理一下思路,就有方法了:
-
从 \(1\) 开始 dfs,
dfs(u)
之前 \(u\) 的左端点已经确定为 \(l_u\)。 -
保存一个全局 \(R\),初始 \(R=1\)。
-
每次进入 \(u\) 节点时,如果 \(u\) 有 \(k\) 个儿子,\(R\) 增加 \(k+1\)。
-
依次遍历所有儿子,对于第 \(i\) 个儿子 \(v_i\),确定 \(l_{v_i}=r_u-i\),并递归 \(v_i\)。
#include<bits/stdc++.h> using namespace std; long long N, head[500050], nxt[1000010], to[1000010], cnt, idx, ANSL[500050], ANSR[500050], stk[500050], len; inline void addedge(long long u, long long v) { nxt[++cnt] = head[u]; to[cnt] = v; head[u] = cnt; } inline void DFS(long long x, long long f) { for (long long e = head[x]; e; e = nxt[e]) if (to[e] != f) DFS(to[e], x); for (long long e = head[x]; e; e = nxt[e]) if (to[e] != f) stk[++len] = to[e]; ANSL[x] = ++idx; while (len) { ANSR[stk[len]] = ++idx; len--; } } int main() { scanf("%lld", &N); for (long long i = 1, u, v; i < N; i++) { scanf("%lld %lld", &u, &v); addedge(u, v); addedge(v, u); } DFS(1, 0); ANSR[1] = ++idx; for (long long i = 1; i <= N; i++) printf("%lld %lld\n", ANSL[i], ANSR[i]); return 0; }
#L. [NOIP2020] 移球游戏
考虑先枚举颜色,然后将此颜色的所有球移动到同一个柱子上。
Step. 0 问题的转换
为了方便,我们将每个柱子看作一列,从左到右标号为 \(1..n\)
记 \(now\) 表示当前枚举到的颜色,令所有 \(a_{i,j}=now\) 的位置为 \(1\),\(a_{i,j}\not=now\) 的位置为 \(0\)
记 \(t_i\) 表示第 \(i\) 列 \(1\) 的个数
这时问题的目标就转化为了将所有的 \(1\) 移到同一列上
Step. 1 制造全 \(0\) 列
考虑对于每个颜色先制造出一个全 \(0\) 列,然后再制造全 \(1\) 列
至于为什么要先制造全 \(0\) 列我后面会讲,这里先讲构造方法:
-
还是为了方便,我们强制第 \(n+1\) 列是空的(没有任何球)
具体操作时可以维护一个数组 \(p\),其中 \(p_i\) 表示当前第 \(i\) 列的柱子在最开始时的编号为 \(p_i\)
这样就可以通过
swap
\(p\) 中的两个数来使柱子的排列方便我们构造 -
设 \(tot=t_1\),将第 \(n\) 列最上面的 \(tot\) 个球移动到第 \(n+1\) 列
-
将第 \(1\) 列中的 \(1\) 全部移动到第 \(n\) 列,\(0\) 全部移动到第 \(n+1\) 列
具体过程就是如果第一列的最上面为 \(1\) 就移到第 \(n\) 列,否则移到第 \(n+1\) 列
此时可以发现第一列中的 \(0\) 和 \(1\) 被我们分离了
-
将第 \(n+1\) 列中的 \(m-tot\) 个 \(0\) 全部移回第 \(1\) 列(其中 \(tot\) 为第一步中的 \(t_1\))
-
将第 \(2\) 列中的 \(0\) 分离到第 \(1\) 列,\(1\) 分离到第 \(n+1\) 列(如果第一列塞不下 \(0\) 了就往第 \(n+1\) 列丢)
此时我们就构造出了一个全 \(0\) 列!
至于第四步中为什么 \(0\) 的数量一定够,是因为前两列中 \(1\) 的个数一定不大于 \(m\),所以 \(0\) 的个数一定 \(\geq 2m-1的数量\) ,即 \(\geq m\)。而这四步的本质也就是将前两列中的 \(0\) 全部分离到第一列中,所以一定能构造出一个全 \(0\) 列
Step. 1
实际上就是用第 \(1,2,n,n+1\) 列构造了一个全 \(0\) 列
Step. 2 构造全 \(1\) 列
我们在 Step. 1
中构造全 \(0\) 列的目的自然是为了方便将所有的 \(1\) 移到同一列
依然是为了方便,我们强制在构造全 \(1\) 列时第 \(n\) 列要为全 \(0\),第 \(n+1\) 列要为空
所以在构造了全 \(0\) 列的之后需要 \(swap(p_1,p_n),swap(p_2,p_{n+1})\) (因为这时第一列为全 \(0\),第二列为空)
然后自然是通过全 \(0\) 列和全空列构造全 \(1\) 列了(不然上一步构造全 \(0\) 列干啥),就像 Step. 1
中的 1.2. 那样将第 \(1\) 列分解到第 \(n,n+1\) 列
然后你就会发现第 \(1\) 列中的 \(1\) 全部被移到了第 \(n\) 列的最上方,然后第 \(n+1\) 列构成了一个新的全 \(0\) 列
所以我们第一步中构造全 \(0\) 列的目的就是保证在这步中,第 \(n\) 列除了最上面的第一列中的 \(1\),其它都为 \(0\)(否则第 \(n\) 列的下方可能出现其它的 \(1\),第 \(n+1\) 列也不一定是全 \(0\))
然后你又能发现你可以用第 \(n+1\) 列新产生的全 \(0\) 列和第 \(2\) 列继续操作,再用新的全 \(0\) 列和第 \(3,4,...,n-1\) 列操作
所有操作完成后,所有的 \(1\) 都被移到了柱子的最上方,这时就可以将所有 \(1\) 都移到空行,然后再用全 \(0\) 列去填充产生的空当
然后我们发现当前枚举的颜色已经合法,剩下的相当于要将 \(n-1\) 根柱子的颜色分离,用同样的方法求解即可
Step. 3 \(n=2\)
也许你以为本题的做法到这里就结束了?其实并没有
你会发现这个做法根本过不了样例
因为当 \(n=2\) 时,第 \(2\) 列和第 \(n\) 列是同一列,也就是说不能用上面的方法制造全 \(0\) 列
所以要特判 \(n=2\) 的情况
这个应该挺好做的,这里简单介绍一下我的做法:
-
还是像
Step. 1
中的 1.2. 那样将第 \(1\) 列分解到第 \(2,3\) 列 -
然后再将分离后的丢回第 \(1\) 列(先 \(1\) 后 \(2\))
这两步操作相当于将第一列排了个序
-
将第三列移回第二列,然后将第一列上面的 \(2\) 移到第三列
-
然后类似
Step. 1
中的 1.2. 那样将第 \(2\) 列分解到第 \(1,3\) 列,就能得到全 \(1\) 列和全 \(2\) 列了
Step. 4 操作次数分析
最外层枚举颜色一个 \(n\) ,每次构造全 \(1\) 列时需要 \(nm+m\)(因为 \(1\) 的总个数为 \(m\),所以分解时的第一步均摊的总次数为 \(m\))
因为列数随着颜色一个一个处理完会减小,所以 \(nm+m\) 中的 \(n\) 其实是个等差数列,也就是 \(\sum\limits_{i=1}^n im+m\)。所以有个 \(1/2\) 的常数
构造全 \(0\) 列时上限需要 \(4m\) 次,所以操作次数上限为 \(\sum\limits_{i=1}^n im+5m\),极限数据满打满算要操作 \(600,000\) 次,可以轻松通过本题
复杂度 \(=\) 操作次数,所以不需要管它
Step. 5 一些小细节
-
所有对第 \(i\) 列的操作实际上都是对 \(p_i\) 进行的,因为上面说第 \(i\) 列只是方便思考,实际上时对 \(p_i\) 进行的操作
-
每次移完球需要动态更新 \(p\) 数组
Step. 6 Coding
#include<cstdio> #include<algorithm> using std::swap; void write(int x) { if (x < 0) { putchar('-'); x = -x; } if (x < 10) putchar(x + '0'); else { write(x / 10); putchar(x % 10 + '0'); } } void print(int x = -1, char c = '\n') { write(x); putchar('\n'); } int L[820005], R[820005], CNT = 0, n, m, a[55][405], cnt[55], tot[55], p[55]; void move(int x, int y) { ++CNT; L[CNT] = x; R[CNT] = y; a[y][++cnt[y]] = a[x][cnt[x]--]; } int count(int x, int y) { int ret = 0; for (int i = 1; i <= m; i++) ret += a[x][i] == y; return ret; } inline int top(int x) { return a[x][cnt[x]]; } int main() { // freopen("ball.in","r",stdin); // freopen("ball.out","w",stdout); scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) { cnt[i] = m; for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]); } cnt[n + 1] = 0; for (int i = 1; i <= n + 1; i++) p[i] = i; for (int now = n; now >= 3; now--) { int tmp = count(p[1], now); for (int i = 1; i <= tmp; i++) move(p[now], p[now + 1]); for (int i = 1; i <= m; i++) if (top(p[1]) == now) move(p[1], p[now]); else move(p[1], p[now + 1]); for (int i = 1; i <= m - tmp; i++) move(p[now + 1], p[1]); for (int i = 1; i <= m; i++) if (top(p[2]) == now || cnt[p[1]] == m) move(p[2], p[now + 1]); else move(p[2], p[1]); swap(p[1], p[now]); swap(p[2], p[now + 1]); for (int k = 1; k < now; k++) { tmp = count(p[k], now); for (int i = 1; i <= tmp; i++) move(p[now], p[now + 1]); for (int i = 1; i <= m; i++) if (top(p[k]) == now) move(p[k], p[now]); else move(p[k], p[now + 1]); swap(p[k], p[now + 1]); swap(p[k], p[now]); } for (int i = 1; i < now; i++) while (top(p[i]) == now) move(p[i], p[now + 1]); for (int i = 1; i < now; i++) while (cnt[p[i]] < m) move(p[now], p[i]); } int tmp = count(p[1], 1); for (int i = 1; i <= tmp; i++) move(p[2], p[3]); for (int i = 1; i <= m; i++) if (top(p[1]) == 1) move(p[1], p[2]); else move(p[1], p[3]); for (int i = 1; i <= tmp; i++) move(p[2], p[1]); for (int i = 1; i <= m - tmp; i++) move(p[3], p[1]); while (cnt[p[3]]) move(p[3], p[2]); for (int i = 1; i <= m - tmp; i++) move(p[1], p[3]); for (int i = 1; i <= m; i++) if (top(p[2]) == 1) move(p[2], p[1]); else move(p[2], p[3]); print(CNT); for (int i = 1; i <= CNT; i++) { print(L[i], ' '); print(R[i]); } return 0; }
本文来自博客园,作者:Naitoah,转载请注明原文链接:https://www.cnblogs.com/LuckyCat-Naitoah/p/17563359.html