11.26
100+40+40+20=200。
总体上感觉还行,B 赛时想了个神秘东西,不过没有实现(事实证明这是正确的选择),但是 C 不会启发式分裂吃大亏。
闲话
一个非常重要的问题是在不会手写哈希表的情况下应该使用什么来当作哈希表。
\(\text{unordered_map}\) 和 \(\text{gp_hash_table}\) 被卡的概率都比较大,而 \(\text{map}\) 又稳定带一个大 \(\log\)。
于是 \(\text{cc_hash_table}\) 似乎成了最好的选择,但是今天 C 题神秘数据把 \(\text{cc_hash_table}\) 卡掉了,这也是第一次见到 \(\text{cc_hash_table}\) 败北。
那么,为什么 STL 里不能有一个好用的哈希表呢?
A.石子游戏
开场硬控 1.5h,试图手玩找规律,发现石子到第三堆就不会了。
于是直接开猜,记录石子数为 \(1,2,>2\) 的堆数,暴力记搜,把所有二十多种可能有用的转移都搜一遍。
或为全场最独特代码
#include <bits/stdc++.h>
#define LL long long
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pLi pair<LL,int>
#define fr first
#define se second
#define DB double
#define LD long double
#define Un unsigned
#define Ve vector<int>
#define pb push_back
using namespace std;
inline LL read()
{
LL x = 0,f = 1;char ch = getchar();
while (!isdigit(ch)) (ch == '-') && (f = -1),ch = getchar();
while (isdigit(ch)) x = x*10+ch-48,ch = getchar();
return x*f;
}
const int N = 105;
int f[N][N][N][2];
bool D(int a,int b,int c,bool fl)
{
if (~f[a][b][c][fl]) return f[a][b][c][fl];
bool res = 0;
if (fl)
{
if (a) res |= !D(a-1,b,c,!fl);
if (b) res |= !D(a,b-1,c,!fl)|!D(a+1,b-1,c,!fl);
if (c) res |= !D(a,b,c-1,!fl)|!D(a,b+1,c-1,!fl)|!D(a+1,b,c-1,!fl);
}
else
{
if (a)
{
if (a > 1) res |= !D(a-2,b,c,!fl);
if (b) res |= !D(a-1,b-1,c,!fl)|!D(a,b-1,c,!fl);
if (c) res |= !D(a-1,b,c-1,!fl)|!D(a-1,b+1,c-1,!fl)|!D(a,b,c-1,!fl);
}
if (b)
{
res |= !D(a,b-1,c,!fl);
if (b > 1) res |= !D(a+2,b-2,c,!fl)|!D(a+1,b-2,c,!fl)|!D(a,b-2,c,!fl);
if (c) res |= !D(a,b-1,c-1,!fl)|!D(a,b,c-1,!fl)|!D(a+1,b-1,c-1,!fl)|!D(a+1,b,c-1,!fl)|!D(a+2,b-1,c-1,!fl);
}
if(c)
{
res |= !D(a+1,b,c-1,!fl)|!D(a,b,c-1,!fl);
if (c > 1) res |= !D(a,b,c-2,!fl)|!D(a,b+1,c-2,!fl)|!D(a+1,b,c-2,!fl)|!D(a,b+2,c-2,!fl)|!D(a+2,b,c-2,!fl)|!D(a+1,b+1,c-2,!fl);
}
}
return f[a][b][c][fl] = res;
}
int main()
{
memset(f,-1,sizeof(f));
int T = read();
while (T--)
{
int n = read(),c1 = 0,c2 = 0;
for (int i = 1,x;i <= n;i++)
{
x = read();
if (x == 1) c1++;
if (x == 2) c2++;
}
puts(D(c1,c2,n-c1-c2,1) ? "Win" : "Lose");
}
return 0;
}
B.树上字符串
记一个绝妙的过不了的赛时想法。
\(|S|\le30\),果断考虑矩阵。询问时直接倍增。时间复杂度 \(O(q|S|^3\log n)\),空间复杂度 \(O(n|S|^2\log n)\),在 \(q=10^5,n=10^5\) 时的表现相当优异!!!
发现倍增时间和空间复杂度都被暴力碾,所以放弃倍增。
对每个点维护到根路径自下而上的矩阵乘积以及矩阵乘积的逆,自上而下的矩阵乘积以及矩阵乘积的逆,对于每次询问 \((u,v)\),找到 \((u,v)\) 的最近公共祖先,然后用我们预处理的数组来求就好。
这样为什么是正确的呢?因为我们每个点的矩阵都是主对角线全为 1 的上三角矩阵,所以一定有逆,而且一个关键的性质是矩阵乘积的逆等于逆的乘积,所以完全正确!
而询问时我们的答案矩阵为 \(1\times|S|\) 的,所以询问复杂度仅为 \(O(q|S|^2)\)。
这时会发现我们的空间复杂度为 \(4n|S|^2\),这个致命的 \(4\) 让我们的空间直接飙过 \(\text{1GB}\),解决方法也很简单,把询问离线下来就只需要一个了。
同时我们每个点上的矩阵都是 \(1\) 的个数为 \(O(|S|)\) 级别的稀疏矩阵,那么我们预处理的时间复杂度为 \(O(n|S|^2)\) 吗?
所以为什么稀疏矩阵的逆矩阵不是稀疏矩阵!!!
当然如果有人会 \(O(n^2)\) 矩阵乘法或者 \(O(n^2)\) 高斯消元的话,这个做法还是可以救一救的。
C: 区间划分
感觉比 B 水啊。
合法区间差不多为 \(O(n\log n)\) 量级的,现在问题是如何快速找出合法区间。
可以根据区间 \(\max\) 来启发式分裂,设区间最大值为 \(b_p\),考虑所有跨过 \(p\) 的区间,则区间和一定在 \(2^{b_p}\sim 2^{b_p + \log(r-l+1)}\) 之间,每次选择 \(\min(i-L_i,R_i-i)\) 进行枚举用哈希表判断即可。
apjifengc's blog 说过 \(\min(i-L_i,R_i-i)\) 是 \(O(n\log n)\) 级别的,所以时间复杂度 \(O(n\log^2 n)\)。