幻影彭的彩虹

寻遍这星空

T1

签到题。

简单观察,发现最后的数为 \(\sum_{i=1}^{n}a_i\times\tbinom{n-1}{i-1} \pmod m\)

题目需要求出无关量,本质上是求系数为 \(0\) 的项,即求 \(\tbinom{n-i}{i-1} \equiv 0\pmod m\)\(i\)

因为 \(m\) 不为质数,所以没法直接处理阶乘,但很容易给出一个递推的 \(O(n^2)\) 做法。

注意到我们只关心 \(\tbinom{n-i}{i-1}\) 是否为 \(m\) 的倍数,\(m\) 的质因子也比较少 \(\leq 10\),所以我们可以直接记录每一个质因子的质数即可,这样就可以直接阶乘。

T2

简单题。

有个很重要的限制,一条边只能经过两次,说白了就是进入一棵子树之后,必须先确定叶节点有没有他的女朋友之后才能出去。

所以就能设计出树形 DP,\(dp[u][0/1]\),表示 \(u\) 的子树是否有渡边女朋友,走完子树 \(u\) 的期望。

最终答案为 \(dp[u][1]\)

考虑转移,首先,有小弟把守的节点,\(dp[u][0]=0\),因为渡边家兴只需要询问小弟就可以走完这棵子树。

计算 \(dp[u][0]\),就是 \(\sum\limits_{v\in E(u)}dp[v][0]+2\),就是搜索完所有的儿子节点加上进入每个儿子节点的消耗。

计算 \(dp[u][1]\),本质上我们是要确定一个搜索子树的顺序,让期望搜索时间最小,考虑对于一个顺序 \(p\),期望时间是多少,记 \(v\) 子树中叶子个数为 \(sz_v\),显然是 \[ \Large\sum\limits_{i=1}^{k} dp[p_i][1]\times \frac{sz_{p_i}}{sz_u} + (dp[p_i][0]+2)\times\frac{\sum\limits_{j=i+1}^{k}sz_{p_j}}{sz_v} \] 即枚举所在子树,加上额外的搜索空子树的时间。

我们可以证明,按照 \(\frac{dp[v][0]+2}{sz_v}\) 升序排序后的顺序是最优的,具体证明参考国王游戏。

T3

有些毒瘤的题。

简单观察发现每个字符都和其上下的位置高度相关,将 NOI 划分为 9 个部分,三个字母均分为中间和两边三个部分,再加上两个空白部分共 11 部分,考虑 DP,设 \(dp[i][s][l][r]\) 表示第 \(i\) 列,状态为 \(s\) ,上下为 \(l,r\) 的最大值。

其它的转移是简单的,我们着重强调 N 中间部分的转移。它转枚举了上一个上下端点 \(l',r'\),向 \(l,r\) 转移,条件为 \(l\le r'+1\),枚举当前的上端点 \(l\),我们可以动态维护一个数组 \(dp\_max[i]\) 表示考虑所有 \(r=i,l'\le l\)。转移使用当前的 \(l\) 对应的 \(dp\_max\) 数组\([1,r]\)\(\max\) 即可。

这道题的思维难度甚至没有 T2 大。

T4

思维题。

找规律和暴力各有 10pts。

Solution1

先把 BFS 序转化为 \(1,2,\dots n\),然后再来考虑。

因为要求平均高度,所以我们需要求出总高度和总个数,转化后一棵树的高度等于 \(n\) 号节点的深度。

现在考虑划分 \(1-n\) 这个 BFS 序列。可以发现一个划分最多只能对应一棵树。

一个显然的必要条件是相同高度的元素在 DFS 序列中递增,另一个显然的必要条件是 DFS 序列中 \(dep_{a_i}+1\ge dep_{a_{i+1}}\)

事实上,这两个条件也是充分的。

\(dp[i][0/1]\) 表示考虑到 \(i\),总数和总高度。转移检查哪些左端点可以转移即可,事实上,可以转移的左端点一定是一个区间,可以处理 \(dp\) 数组的前缀和完成快速转移。

判断转移区间和结论证明,留作思考,后文有 Details。

如果可能,尽量不要看 Details,自行思考

Solution2

考虑一个划分合法的必要条件,Solution1 中已经提到。

考虑哪些位置可以划分,把一个间隔看成一个 01 变量,选择划分为 \(1\),发现可以转化为一个这样的问题:

你需要确定长度为 \(n-1\) 的二进制串,有若干个限制。每个限制形如

  • \([l,r]\) 至多有一个 \(1\)。即:\(dep_{a_i}+1\ge dep_{a_{i+1}}\) 转化而来的 \([a_i,a_{i+1})\)
  • \(x\) 位置必须为 \(1\) 。即:如果 \(pos_i>pos_{i+1}\),那么由于同高度在 \(a\) 中的位置递增,则 \(i\) 位置必须为 \(1\)

很容易设计出一个 \(O(n^2)\) 的动态规划做法。

观察第一个条件,如果 \(a_{i+1}>a_i\) 才会有第一个限制,如果 \(a_i+1=a_{i+1}\),那么相当于这个限制不存在。

否则,因为保证一定存在一棵合法的树,所以 \([a_i,a_{i+1}]\) 区间的 \(pos\) 数组必须能够被划分为两个连续上升子序列。\(a_i\) 又和 \(a_{i+1}\) 紧紧挨在一起,那么得知必定会被划分,因为中间的某个数一定会冲突。我们很容易模拟得到被划分的位置,之后这个区间的所有数都不能再被划分。

对于没有限制的位置,我们划分与不划分的方案数是相同的,因此对期望的贡献是 \(0.5\)

将所有位置的期望加起来就是答案。

Solution1 Details

Transform

看看样例,发现从 \(1-n\) 排列比较好想,所以考虑先把点做个变换,弄成 \(1-n\),所以 \(b\) 变成了 \(1,2,3,\dots,n\)

然后记 \(u\) 的深度为 \(dep_u\),发现 \(\forall i\in[1,n),dep_i\le dep_{i+1}\le dep_i+1\)

然后又发现,对于 BFS 序列为 \(1,2,3,\dots n\) 的树,一个点遍历子节点的顺序一定是按编号从小到大遍历

所以,确定了每个点的深度和 DFS 序列。可以唯一确定一颗树。

感性证明的话,确定深度之后把点画出来,画在一个二维平面上,深度相同的点在一层按编号从小到大排列,然后在 DFS 序列上走,如果下一个点深度更大,那肯定是往下连,如果深度不变或者更小,那一定是回去了一部分再往下走了一个,这样的逻辑可以唯一确定一棵树。

自上到下,自左到右遍历二维平面所有点的过程,就是 BFS 的过程

请认真理解二维平面的含义。

内含比较严格的证明

本质上,确定深度的过程就是划分 \(b\) 序列的过程

Native DP Algorithm

所以考虑对 BFS 序列的划分过程 DP,设 \(dp[i][k]\) 表示考虑到第 \(i\) 个点,深度为 \(k\) 的合法划分方案总数。

然后我们需要枚举一个左端点 \(j\),考虑如何判断这个划分是否合法。

首先有个必要条件:同一深度的点,在 DFS 序列上的位置必须递增,因为我们遍历一个点的儿子的顺序是从小到大。称该条件为条件一

其次,我们模拟一下在 DFS 序列上走的过程,发现一个点 \(u\) 的下一个点 \(x\) 一定有 \(dep_u+1\ge dep_x\),原因是显然的。称该条件为条件二

满足了以上条件,我们可以说明一定可以构造出一棵唯一对应的树。具体的,对于一个点 \(u\),下一个点是 \(v\),找到它或者它的祖先 \(u'\), 满足 \(dep_v=dep_{u'}+1\),那么 \(v\) 的父亲就是 \(u'\)。由于第一个条件,限制了处理的 \(v\) 一定是该层第一个还没有安排父亲的点。所以每个点只会恰好被安排一次父亲,得到一个合法的树。

这样的话,我们再记录一维划分起点,变成 \(dp[i][j][k]\)

转移枚举当前起点和上一个划分的起点,判断两个条件可以简单的 \(O(n)\) 做。

复杂度为 \(O(n^4)\),因为对于 \(n\) 个深度 \(k\) ,一共只需要判断一次。

Observaion1&&Optimization1

  • 发现其实不用记录具体每个深度有几个元素,只需要记录深度之和与树的个数就可以计算答案。
  • 假设划分的区间为 \([l,r]\) ,深度为 \(d\)。条件二等价于,\(a\)\([1,l)\) 的后一个元素 \(x\),一定满足 \(x\le r\)。因为每次加入一段区间的点之后,上一次的深度为 \(d-1\) 的点的右端点,如果还没有确定深度,就会被确定为 \(d\)。所以加入后,存在右端点还没确定深度的点,其本身深度只能为 \(d\) 了。所以可以直接判断交界处的深度关系,判断方式是 \(\forall i\in[1,n),a_i>j \or a_{i+1}\ge i\)

40Pts 的代码运用了第二个观察,请阅读。

运用第一个观察,DP 状态简化为 \(dp[i][j][0/1]\)

再次运用第二个观察,其实已经无需记录 \(j\),DP 状态进一步简化为 \(dp[i][0/1]\)

上述做法的复杂度是 \(O(n^3)\) 的,考虑优化。

参看 Codes 部分的 40Pts 做法。\(sum\) 数组的含义是,\(sum[i][j]\) 表示以 \(i\) 结尾,深度为 \(j\) 的方案总数。

40Pts 的部分没有对层数做简化

Observaion2&&Optimization2

  • 其实 \(dp\) 数组没有用,只需要记录一个 \(sum\) 数组就可以了。

  • 条件一,显然可行的 \(j\) 是一个右端点为 \(i\) 的区间,对于每个 \(i\),可以处理出满足 \(p\) 数组(参考题解开头的定义)区间递增的最小左端点,作为 \(j\) 左端点的限制。

  • 条件二,显然可行的 \(j\) 是一个前缀,并且右端点随 \(i\) 增大,这限制了 \(j\) 的右端点。

由于条件一,二的限为区间转移限制,所以记录一下 \(sum\) 的前缀和即可做到转移 \(O(n)\)

利用尺取法的思想可以 \(O(n^2)\) 的计算条件二的右端点。

但进一步观察,发现对条件二进行了一些无用的 check,每次移动端点时,有用的 check 只有一个,就是值为右端点本身的位置。

所以 check 变成了 \(O(1)\),总复杂度 \(O(n)\)

参考 100Pts 代码,注意,其中的 \(dp\) 代表 40pts 写法中的 \(sum\)\(sum\) 代表其前缀和。

Notes

  • DP 过程中最大值达到了 \(2^{n}\),需要手写科学计数法,可以忽略指数差距过大的加减运算。

Codes

40pts \(O(n^3)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<bits/stdc++.h>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}const int N=205;
int i,j,k,n,s,t,m,tp1,tp2;
int a[N],p[N],b[N];
double dp[N][N][N],sum[N][N];
signed main()
{
read(n);
for(i=1;i<=n;i++)read(a[i]);
for(i=1;i<=n;i++)read(b[i]),p[b[i]]=i;
for(i=1;i<=n;i++)a[i]=p[a[i]];
for(i=1;i<=n;i++)p[a[i]]=i;
dp[1][1][1]=1;sum[1][1]=1;
for(i=2;i<=n;i++){
for(j=2;j<=i;j++){
//条件1
for(k=j+1;k<=i;k++)
if(p[k-1]>p[k])break;

if(k!=i+1)continue;

//条件2
for(k=1;k<n;k++)
if(a[k]<j&&a[k+1]>i)break;
//如果交界处,一个深度小于 d,另一个大于 d,那么寄。
if(k!=n)continue;

for(k=1;k<=n;k++)
dp[i][j][k]+=sum[j-1][k-1];
}
for(j=1;j<=i;j++)
for(k=1;k<=n;k++)
sum[i][k]+=dp[i][j][k];

}
double ans=0,cnt=0;
for(i=1;i<=n;i++){
cnt+=sum[n][i];
ans+=sum[n][i]*i;
}
printf("%0.3lf",ans/cnt);
return 0;
}

100 pts \(O(n)\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<bits/stdc++.h>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
const int N=202005;
int i,j,k,n,s,t,m,tp1,tp2;
int a[N],p[N],b[N],lst[N],far[N];
struct Double{
double val;
int p;
Double cap(Double x){
while(abs(x.val)>1e18){
x.val/=2;
x.p++;
}
while(abs(x.val)<1e-18){
x.val*=2;
x.p--;
}
return x;
}
void operator =(int x){p=0,val=x;}
Double operator +(const Double &x){
if(x.p-p>50)return x;
if(p-x.p>50)return *this;
return cap({val+x.val*pow(2,x.p-p),p});
}
void operator +=(const Double &x){*this=*this+x;cap(x);}
Double operator -(){return Double{-val,p};}
Double operator -(Double &x){return cap((*this)+(-x));}
Double operator /(const Double &x){return cap({val/x.val,p-x.p});}
double get(){return val*pow(2.0,p);}
};
Double dp[N][2],sum[N][2];
signed main()
{
read(n);lst[1]=1;
for(i=1;i<=n;i++)read(a[i]);
for(i=1;i<=n;i++)read(b[i]),p[b[i]]=i;
for(i=1;i<=n;i++)a[i]=p[a[i]];
for(i=1;i<=n;i++)p[a[i]]=i;
for(i=2;i<=n;i++){
if(p[i]>p[i-1])lst[i]=lst[i-1];
else lst[i]=i;
}
dp[1][0]=1,dp[1][1]=1;
sum[1][0]=1,sum[1][1]=1;
far[1]=1;
for(i=2;i<=n;i++){
for(j=far[i-1]+1;j<=i;j++){
if(a[p[j-1]+1]<=i||p[j-1]==n)continue;
else break;
}
far[i]=--j;
if(far[i]>=lst[i]){
dp[i][0]=sum[j-1][0]-sum[lst[i]-2][0];
dp[i][1]=(sum[j-1][1]-sum[lst[i]-2][1])+(sum[j-1][0]-sum[lst[i]-2][0]);
}
sum[i][0]=dp[i][0]+sum[i-1][0];
sum[i][1]=dp[i][1]+sum[i-1][1];
}
printf("%0.3lf",(dp[n][1]/dp[n][0]).get());
return 0;
}

树形背包的比较严谨的复杂度证明

转移方式

1
2
3
4
5
6
7
8
9
10
11
12
void dfs(int u,int fa=0){
sz[u]=1;
for(auto v:e[u]){
if(v==fa)continue;
dfs(v,u);sz[u]+=sz[v];
for(i=min(k,sz[u]);i>=0;i--){
for(j=max(0,i-(sz[u]-sz[v]));j<=i&&j<=sz[v];j++){
//O(trans) do something
}
}
}
}

证明,总复杂度为 \(O(n^2)\)

证明 \(n^2\) 直接忽略 \(i\) 的循环和 \(k\)\(\min\) 的操作。

考虑在某个点合并子树的过程,本质上,如果把 \(sz_u\) 看成当前子树内所有的点,我们的第一个循环是限制了合并后的总大小,第二个循环限制了已经合并的子树的大小,以及待合并的子树的大小。

所以在一个点 \(u\) 处,只有每一对以它为 LCA 的点对会贡献一次,其它点对均不贡献。

所以总复杂度为点对数量 \(O(n^2)\)

证明,总复杂度为 \(O(trans\times min(n^2,nk))\)

\(O(n^2)\) 的证明是老生常谈的事情了,考虑 \(k\leq n\) 的情况,试证明总复杂度为 \(O(nk)\)

考虑一对点在 LCA 处的贡献,

考虑合并子树时的三种 case,小于 \(k\) 向大于 \(k\) 合并,大于 \(k\) 向大于 \(k\) 合并,小于 \(k\) 向小于 \(k\) 合并。

我们分别说明三种情况的时间复杂度都是 \(O(nk)\) 的。

Case1

小于合并到大于。

准确的说,是小于 \(k\) 的子树合并之后,大小大于 \(k\)

考虑每一个点对这种情况的贡献,每个点显然只会参与一次过程,因为参与之后它所在的块的大小就大于 \(k\) 了。

总复杂度 \(O(nk)\)

Case2

大于合并到大于。

考虑每一次合并,复杂度 \(O(k^2)\),会永远失去一个大于 \(k\) 的块。而我们最多会生成 \(\frac{n}{k}\) 个大小大于 \(k\) 的块,因为一个点至多参与一次生成新块的过程。

总复杂度 \(O(nk)\)

Case3

小于合并到小于,那么这种情况就只会发生于大小小于 \(k\) 的子树内以及其父亲的子树间。如果一个点的父亲的大小也不超过 \(k\),我们忽略它本身,直接在它的父亲处计算所有复杂度贡献。这样所有的复杂度分成两个部分,小于 \(k\) 的子树内,以及其兄弟间(也就是在一棵很大的子树中,有几个比较小的儿子先合并了)。

第一部分的复杂度显然是 \(\leq nk\) 的(本质上是选了几个自身不超过 \(k\),和不超过 \(n\) 的变量求平方和,参考 \(n^2\) 的证明)

第二部分,每个点只会参与一次这个过程,因为参与一次之后它所在的块大小就大于 \(k\) 了,所以这一部分的总复杂度也是 \(O(nk)\)

综上所述,树形背包的复杂度为 \(O(nk)\)

思考

在上面的代码的边界做一些改动,哪些改动会让复杂度假掉。

CF1709 总结和题解

A

B

C

题意

有个合法括号序列,部分字符被 ? 替换了,问是否存在唯一的一种? 的方案,使得括号序列合法,即判断填 ? 使得括号序列合法的方案数是否等于1。存在唯一方案输出 YES,方案不唯一输出 NO

序列长度 \(\sum n\le 2\times 10^5\),测试点数 \(T\leq 5\times 10^4\)

第一行输出测试点总数 \(T\)

之后每一行一个字符串 \(s\) 表示替换掉部分字符后的合法括号序列

题解

考虑一个括号序列,令 ( 为 1,) 为 -1,记形成的新序列为 \(c\),然后对该序列做前缀和得到 \(s\),容易发现括号序列合法的充要条件是 \(s_n=0\)\(\forall i\in[1,n],s_i\ge0\)

已知给定的带 ? 的序列是一个合法序列变来的,所以考虑统计三种字符的个数,?,(,) 分别个数记为 cnt cnt0 cnt1,发现如果出现 cnt + min(cnt0,cnt1)=n/2 一定是 YES,因为每个 ? 的填法已经确定了。

因为一定要合法,所以 \(s_n\) 一定为 0,现在就需要让每一个 \(s_i\) 尽量大,方法就是把 ( 尽量往前面填,这样会让每个 \(s_i\) 都尽可能大。

然后我们考虑一种稍稍不那么优秀的方案,就是交换一组由两个 ? 构成的 () 序列,发现交换 () 交界处的那两个一定最优,所以求一下交界处的 min 看看是否大于 2 就行。

可以证明,如果这样交换不行,那么其它任何交换方案都不行。

我写麻烦了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=1e6+10;
int i,j,k,n,s,t,m,tp1,tp2;
char ch[N];
int c[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(t);
while(t--){
scanf("%s",ch+1);
n=strlen(ch+1);int cnt=0,cnt0=0,cnt1=0;
for(i=1;i<=n;i++){
if(ch[i]=='?')cnt++;
if(ch[i]=='(')cnt0++;
if(ch[i]==')')cnt1++;
}
if(cnt<=1||cnt+min(cnt0,cnt1)==n/2){
puts("YES");
continue;
}
int flag=0,top=0,t1=0;
for(i=1;i<=n;i++){
if(ch[i]=='(')top++;
if(ch[i]==')')top--;
if(ch[i]=='?'&&cnt0!=n/2)top++,t1++,cnt0++;
else if(ch[i]=='?'&&cnt0==n/2)top--;
c[i]=top;
}
int lst=0,min_val=INF,all=0;
for(i=1;i<=n;i++){
if(ch[i]=='?'){
all++;
if(min_val>=2&&all==t1+1)flag=1;
lst=i;
min_val=INF;
}
cmin(min_val,c[i]);
}
if(flag)puts("NO");
else puts("YES");
}
return 0;
}


D

题意

有一个 \(n\times m\) 的网格,第 \(i(i\in[1,m])\) 列的 \([1,a_i]\) 行被锁定了。

你有个机器人,你可以给它发命令,让它向上下左右移动一格,但是机器人有 Bug,你发的每个命令都会被执行 \(k\)不是瞬移 \(k\) 格,而是走 \(k\) 次,每次一格。在任何一个时刻,机器人都不能处于被锁定的格子或者网格外。

给定 \(q\) 组询问,每组询问给定五个参数 \(x_s,y_s,x_f,y_f,k\),代表起点终点坐标和参数 \(k\),问能否从起点到终点,能输出 YES,不能输出 NO

第一行输入 \(n,m\)

第二行输入一个长度为 \(m\) 的数组表示 \(a\)

第三行一个整数 \(q\)

接下来 \(q\) 行每行 \(5\) 个整数 \(x_s,y_s,x_f,y_f,k\) 描述一个询问。

$ 1 n ^9 ; 1 m ^5 ; 1q ^5$

\(a[y_s] < x_s \le n ; 1 \le y_s \le m ; a[y_f] < x_f \le n; 1 \le y_f \le m; 1 \le k \le 10^9\)

题解

假设全部点都没有 \(lock\),考虑从 \((x_1,y_1)\) 能到 \((x_2,y_2)\) 的充要条件,显然是 \(k|abs(x_1-x_2)\) 并且 \(k|abs(y_1-y_2)\)

现在加入限制。允许 \([n-a_i,n]\) 走太麻烦了,翻转一下,变成允许 \([1,a_i']\) 走。

然后最优方案就是先走到当前的最底下,然后横着走过去。

所以找到最低能走到哪个位置,然后做个区间 min,判断最低位置 \(x\) 和区间 min 的关系,如果 \(x\le \min\),显然可以,否则不行。

区间 min 用 ST 表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=4e5+10;
int i,j,k,n,s,t,m,q;
int st[25][N],lo[N];
int ask(int l,int r){
if(l>r)swap(l,r);
int len=lo[r-l+1];
return min(st[len][l],st[len][r-(1<<len)+1]);
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(m),read(n);

for(i=2;i<=n;i++)lo[i]=lo[i>>1]+1;
for(i=1;i<=n;i++)
read(st[0][i]),st[0][i]=m-st[0][i];

for(i=1;i<=20;i++)
for(j=1;j+(1<<i)-1<=n;j++)
st[i][j]=min(st[i-1][j],st[i-1][j+(1<<i-1)]);
read(q);
for(i=1;i<=q;i++){
int x1,x2,y1,y2;
read(y1),read(x1),read(y2),read(x2),read(k);
if(k==0){
if(x1==x2&&y1==y2)puts("YES");
else puts("NO");
continue;
}
y1=m-y1+1,y2=m-y2+1;
if(abs(x1-x2)%k||abs(y1-y2)%k||ask(x1,x2)<(y1%k==0?k:y1%k))puts("NO");
else puts("YES");
}
return 0;
}


E

题意

你有一棵无根树,点数为 \(n\),每个点有个点权 \(a_u\),定义一条路径 \(P(u,v)\) 的权值为经过的所有点的点权的异或和。定义一棵树是合法的,当且仅当树上所有简单路径(只经过每个点一次的路径)的的权值都不为 \(0\)

你可以对权值进行修改,可以改成任意正整数,问最少修改多少次才能让这棵树合法。

输出最小修改次数。

\(n\leq 2\times 10^5,a_i\leq 2^{30}\)

题解

发现路径权值为 \(sum[u] \bigoplus sum[v] \bigoplus a[lca(u,v)]\)\(sum[u]\) 表示从 \(1\)\(u\) 的路径上所有点点权的异或和,每一条路径,我们在路径 \(lca\) 处考虑是否不合法(权值为 0)。

我们称一个点不合法当且仅当有一条 \(lca\) 为这个点的不合法路径经过它。我们从深到浅处理每一个不合法的 \(lca\)

如果一条路径不合法,修改方案可以分为改 \(lca\) 和非 \(lca\),可以说明改 \(lca\) 是最优的。首先,可以说明,改一个点 \(u\) 的最优方案之一是改成 \(2^{u+32}\),因为这样的话所有经过这个点的路径都一定合法。然后说明改 \(lca\) 最优。如果不改 \(lca\),以这颗子树中的点为起点的路径仍然有可能不合法,而改 \(lca\),以这颗子树中的点为起点的路径一定都合法。我们假设了当前处理的 \(lca\) 是最深的,所以这颗子树内所有的不合法路径一定经过了 \(lca\),所以改了 \(lca\),所有路径就一定合法了。所以\(lca\) 最优

怎么判断一个 \(lca\) 处有不合法路径,拿个 set 维护下每颗子树内的 \(sum\),然后加入一颗子树时,枚举加入子树的所有点 \(v\),判断当前 set 中是否存在元素 \(a_[lca]\bigoplus sum[v]\),如果存在,\(lca\) 就不合法,我们给答案加一,在这颗子树的父亲计算时忽略该子树。

发现合并 set 的代价是 \(O(\sum size_u)=O(n^2)\) 的,但是可以启发式合并。

具体的,一个点 \(u\) 继承它最大的儿子的 \(set\),然后在把它自己和其它子树加入到这个 set 中,同时判断是否存在不合法路径。考虑这样的复杂度,证明和树链剖分的复杂度证明相同,继承操作可以用 std::swap,这个函数交换两个 STL 容器的复杂度,在 C++11 以上标准是 \(O(1)\) 的。

对于一个点,我们考虑它什么时候会被合并一次(就是作为非最大子树,被合并上去)。每一个点所在的 set,被合并时,它所在的新 set 的大小至少变为 \(2\) 倍(因为新 set 最开始的大小一定比该点所在的 set 大,否则该点所在的 set 会作为最后保留的 set 而不对复杂度做贡献),最后的 set 大小为 \(n\),所以每个点合并 \(\log n\) 次。

总复杂度 \(O(n\log^2 n)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=2e5+10;
int i,j,k,n,s,t,m,tp1,tp2,ans;
int fa[N],sz[N],son[N],a[N],sum[N];
vector<int> e[N];
set<int> st[N];
void pre_dfs(int u){
sz[u]=1;
for(auto v:e[u]){
if(fa[u]==v)continue;
fa[v]=u;sum[v]=sum[u]^a[v];
pre_dfs(v),sz[u]+=sz[v];
if(sz[v]>sz[son[u]])son[u]=v;
}
}
void redfs(int u,int flag=0){
if(son[u]){
redfs(son[u]);
swap(st[son[u]],st[u]);
}
if(st[u].find(sum[u]^a[u])!=st[u].end())flag=1;
st[u].insert(sum[u]);
for(auto v:e[u]){
if(v==son[u]||v==fa[u])continue;
redfs(v);
for(auto val:st[v]){
if(st[u].find(val^a[u])!=st[u].end())
flag=1;
}
for(auto val:st[v])
st[u].insert(val);
}
if(flag)ans++,st[u].clear();
}
signed main()
{
read(n);
for(i=1;i<=n;i++)read(a[i]);
sum[1]=a[1];
for(i=1;i<n;i++){
int x,y;read(x),read(y);
e[x].push_back(y),e[y].push_back(x);
}
pre_dfs(1);
redfs(1);

cout<<ans<<endl;
return 0;
}


F

题意

定义二进制串为只包含 \(01\) 的字符串。

给你 \(n,k,f\)\(1\le n\leq 15,1\le f,k\leq 2\times 10^5\),你需要给每个长度不超过 \(n\) 的二进制串 \(s\) 确定一个 \(c_s\in [0,k]\) 的权值。然后你需要选出一个只包含长度恰好\(n\) 的二进制串的可重集合,使得这个可重集合最大。并且满足对于所有长度小于 \(n\) 的二进制串 \(s\),集合元素中,\(s\) 作为集合中元素前缀的次数不超过 \(c_s\)

求安排 \(c\) ,使得集合最大大小恰好为 \(f\) 的方案数,对 \(998244353\) 取模。

题解

考虑取到最大值 \(f\) 的条件,不妨令 \(n=1\),发现要求就是 \(c_0+c_1=f\),然后我们考虑一些长度为 \(n\) 的二进制串。

因为要对每个长度小于 \(n\) 的串都确定一个 \(c\),然后我们又发现,记一个前缀 \(s\) 的数量限制为 \(w_s\),实际上 \(w_s=\min(c_s,w_{s+'0'}+w_{s+'1'})\)

然后我们就可以对一个确定的 \(c\) 序列,像树形 \(dp\) 一样算出最大大小。

考虑对这个树形 \(dp\) 的过程计数,本质上,由于树是满二叉树,所以 dp 出每个值的 \(c\) 序列的方案只于层数有关。

所以设 \(dp[i][j]\) 为考虑到深度为 \(n-i\) 的满二叉树,值为 \(j\) 的安排方案总数。

转移限制有两种,第一个是 \(c\) 限制,第二种是子树限制,考虑先算一个 \(sum[i][j]\) 表示子树和为 \(j\) 的方案数。

然后可以得到 \(dp[i][j]=sum[i][j]*(k-j+1)+\sum\limits_{t> j}sum[i][t]\)。分别表示两种限制的贡献。

算出 \(sum\) 后是 \(O(k)\) 转移的。考虑 \(sum[i][j]=\sum\limits_{s+t=j} dp[i-1][s]\times dp[i-1][j]\)。就是个卷积,NTT 板子上去就行。

注意边界,处理最顶层的时候,\(dp[n][i]\) 只能由子树和来限制,因为这个点本来就是长度为 0 的串,不能填 \(c\),我在这里想当然的认为只要 \(i\le k\) 的时候有值,所以没有 AK,实际上这里 \(k< i\le 2k\) 也是有值的,因为这个点本身不会限制。

如果 TLE 了换成 64bit 的机子再交一次,64 bit 的 long long 运算要快一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define int long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=1<<19,mod=998244353;
namespace NTT{
const int N=1<<20,mod=998244353,g=3,gi=332748118;
int i,j,k,n,s,t,m;
int a[N],b[N],rk[N];
int quick(int a,int s,int ret=1){
while(s){
if(s&1)ret=ret*a%mod;
a=a*a%mod,s>>=1;
}
return ret;
}
void NTT(int *a,int type)
{
for(i=0;i<1<<s;i++)
if(rk[i]>i)swap(a[rk[i]],a[i]);
for(int len=1;len<=s;len++)
{
int w=1,wn=quick(g,mod-1>>len);
if(type==-1)wn=quick(wn,mod-2);
for(j=0;j+(1<<len)<=1<<s;j+=1<<len,w=1)
{
for(k=j;k<j+(1<<len-1);k++,w=1ll*w*wn%mod)
{
int x=a[k],y=a[k+(1<<len-1)];
a[k]=(x+1ll*w*y%mod)%mod,a[k+(1<<len-1)]=(x-1ll*w*y%mod)%mod;
}
}
}
}
void init(int nn,int c[],int res[]){
n=m=nn;
memset(a,0,sizeof(a)),memset(b,0,sizeof(b));
for(i=0;i<=n;i++)a[i]=c[i];
for(i=0;i<=m;i++)b[i]=c[i];
while(1<<s<=n+m)s++;

for(i=1;i<1<<s;i++)rk[i]=rk[i>>1]>>1|(i%2?1<<s-1:0);
NTT(a,1),NTT(b,1);
for(i=0;i<1<<s;i++)a[i]=1ll*a[i]*b[i]%mod;
NTT(a,-1);int inv=quick(1<<s,mod-2);
for(i=0;i<=n+m;i++)
res[i]=(1ll*a[i]*inv%mod+mod)%mod;
}
}
int i,j,k,n,s,t,m,tp1,tp2,f;
void Inc(int &a,int b){
a+=b;if(a>=mod)a-=mod;
}
int quick(int a,int s,int ret=1){
while(s){
if(s&1)ret=ret*a%mod;
a=a*a%mod,s>>=1;
}
return ret;
}

int dp[16][N<<1],sum[16][N<<1],p[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
read(n),read(k),read(f);
for(i=0;i<=k;i++)dp[0][i]=1;
for(i=1;i<=n;i++){
NTT::init(k,dp[i-1],sum[i]);
for(j=0;j<=2*k;j++){
if(i!=n)dp[i][j]=sum[i][j]*(k-j+1)%mod;
else dp[i][j]=sum[i][j];
}
if(i!=n)for(j=2*k-1;j>=0&&i!=n;j--){
if(j<=k)Inc(dp[i][j],sum[i][j+1]);
Inc(sum[i][j],sum[i][j+1]);
}
}
cout<<(dp[n][f]+mod)%mod;
return 0;
}


CF1706 小总结

其实都会做,但是各种各样的原因导致了没在场上 AK

ABC

A 题没考虑清楚就在写,WA 了一发样例,然后慢悠悠调好交上去过。

B 题愣了一会儿,弄了个伪 DP 上去,稍微有点磨蹭,但还好。

C 题做得无可挑剔。

D

D 题先想到可以枚举一个端点,二分另一个,\(O(n)\) check,但是发现枚举一个端点之后,可以快速计算另一个端点的最优值,得到一个 \(O(n^2)\) 的枚举 max 的做法过了 D1,然后感觉其实只需要查 \([\lfloor\frac{\max a_i}{k}\rfloor,\lfloor\frac{\max a_i}{k}\rfloor+\sqrt N]\)\(\{\lfloor\frac{\max a_i}{x}\rfloor,x \le k\}\),写好交上去过了 Pretests,但是 FST 了。

后面想到其实对 min 分类限制更多。暴力算 \(\min \lfloor \frac{a_i}{k_i}\rfloor\in [1,B]\),然后对于 \(\min \lfloor \frac{a_i}{k_i}\rfloor\in(B,k]\),可以发现 \(p_i\in [1,\lceil\frac{10^5}{B}\rceil]\),考虑维护 max 的变化,可以用一个 vector 记一下 \(\lfloor\frac{a_i}{p_i}\rfloor\) 的所有可能,共 \(n\times \lceil\frac{10^5}{B}\rceil\) 种,因此对于 \((B,k]\)\(\lfloor\frac{a_i}{p_i}\rfloor\) 的变化总量是 \(O(n\sqrt n)\) 的。

换一种思路考虑,还是枚举 \(x\),令 \(\lfloor\frac{a_i}{p_i}\rfloor\le x\),易知 \(p_i\ge\lfloor\frac{a_i}{x+1}\rfloor+1\),考虑每一个 \(a_i\) 的最好的 \(p_i\),不太可行。但是发现上式中能取到的 \(p_i\) 满足 \(p_i\le\lfloor\frac{N}{x}\rfloor\) 。于是考虑每一个 \(p_i\) 可以让哪些 \(a_i\) 值取到最优,这是一个区间。而我们要考虑的是 min,所以取区间中最小的计算就行。

对于一个 \(x\),区间个数是 \(\frac{N}{x}\) 的,因此总复杂度 \(\sum\limits_{i=1}^N \frac{N}{i}=n\ln n\)

E

E 题在考场上想到了可以类似整体二分的做,先给所有的二分,然后再合并一次,查询每一个询问是否可行。查询的维护可以 dsu on tree 带一个维护线段的 set,复杂度 \(O(n\log^3n)\),感觉能信仰,就写了,可惜没交上去。赛后发现其实有些边界的小错,但是交了还是 TLE 了。告诉我们 set 操作搞个 \(10^7\) 问题还是比较大。

后面发现可以将询问直接塞进 dsu 的过程中,但是处理询问只能处理较小的那一边,如果是小的合到大的上面去,此时询问挂在大的上面,会非常的恼火。于是就把询问拆到两个上面去,又发现可能两个询问会在合并时变成一个但是还没解决,所以合并的时候如果变成一个就又找了一个还没合并的把询问扔上去。

然后又 TLE 了,赛后仔细分析复杂度,发现不对劲,比如有个 \(O(n)\) 的点,然后又有个略小于它但主要是询问的点,然后合并的时候把询问全部扔到另一个点上去了,然后另一个点同这个 \(O(n)\) 点合并的时候又把询问全部扔了,询问就扔了 \(O(n^2)\) 次,寄。

然后想到可以把线段合并的过程记下来,这个就是严格 \(O(n\log n)\) 级别了,后面做一个二维偏序回答询问。成功 AC。

其实 E 题和最近做的题都暴露出几个问题,思维模式僵化,没有办法 Think out of box,同时代码中常常犯一些不容易注意到的小错误,依赖数据调试。

Sum up

关于代码小错的问题,我觉得一边写一边静态查是一个比较理想的解决方案,先认真核实思路的正确性,再检查代码和思路是否一致,同时可以在关键点插入数据输出。

思维上的问题,估计还只有多做题。

收获 1:Dsu on tree 的复杂度理解

Dsu on tree,复杂度证明的过程是这样的:考虑一个点的权值 \(w_i\),当合并时,复杂度贡献为 \(O(\min(w_u,w_v)\),将 \(w_i\) 视为 \(w_i\) 个元素。考虑每个元素的贡献次数,发现每个元素仅在其所在集合大小翻倍时贡献。所以复杂度正确。

因此,权值的构成不影响其时间复杂度

如果在合并的时候进行了额外的对 \(w_i\) 的修改,比如我的错误做法,时间复杂度就应该认真分析了。

Kruscal 重构树的应用和连通性

E 题可以用 Kruscal 重构树做,具体的,先做最小生成树,顺便合并点,现在一个连通块就是一个点,合并两个连通块时,新建一个点,点权为这条边的权值,作为被合并的两个点的父亲,任意两个点联通的时最小权值就是它们的 lca,然后区间 lca 应该都会。事实上甚至没有必要做区间 lca,我们考虑 \(i,i+1\) 什么时候联通,设其时间为 \(f(i)\),可以发现 \(ans(a,b)=\max\limits_{i\in[a,b)} f(i)\),证明比较容易,首先必须取到这个值,否则必然有两个点不连通,其次,如果取到这个值,那么一定都联通,over ,复杂度 \(O(n\log n)\)

已经知道这个性质了,那么,将询问放到 dsu 合并过程中的方法也不难写了,合并时处理其中一个询问即可。

LCA 的一些性质

从 2 中的性质,我们总结出了一些 LCA 的共有性质。

设树上一个点集为 \(S\),定义 \(\operatorname{lca}(S)\) 为点集 \(S\) 中所有点的最近公共祖先。定义符号 \(\operatorname{lca}(a_1,a_2\cdots a_n)\) 表示 \(\operatorname{lca}(\{a_1,a_2,\cdots a_n\})\)

\(\text{Lemma 1.1}\)

\(\operatorname{lca}(S\cup u)=\operatorname{lca}(\operatorname{lca}(S),u)\)

\(\text{Proof 1.1}\)

\(\operatorname{lca}(S)\)\(u\) 的祖先,则结论显然成立。

否则,\(u\) 位于 \(\operatorname{lca}(S)\) 的子树外,\(\operatorname{lca}(S\cup u)\) 必须同时为 \(\operatorname{lca}(S)\)\(u\) 的祖先,故结论同样成立。

\(\text{Theorem 1.2}\)

对于 \(S=\{a_1,a_2\cdots,a_n\}\),满足 \(\operatorname{lca}(S)\in \bigcup\limits_{i=1}^{n-1}\operatorname{lca}(a_1,a_2)\)

\(\text{Proof 1.2}\)

\(S\) 集合大小归纳,设 \(|S|=n\),且对于 \(k\le n\),结论成立。

由 Lemma 1.1,有 \(\operatorname{lca}(S\cup u) = \operatorname{lca}(\operatorname{lca}(S),u)\)

  • 若满足 \(\operatorname{lca}(S\cup u)=\operatorname{lca}(S)\),对于 \(S\cup u\),即 \(|S'|=n+1\),结论成立。
  • 若不满足 \(\operatorname{lca}(S\cup u)=\operatorname{lca}(S)\),记 \(\operatorname{lca}(S)=w\),则有 \(\forall a_i\in S,\operatorname{lca}(a_i,u)=\operatorname{lca}(w,u)=\operatorname{lca}(S\cup u)\),因此对于 \(|S'|=n+1\),结论仍然成立。

证毕。

\(\text{Lemma 2.1}\)

\(\operatorname{dfn}(u)\) 表示 \(u\) 的 dfs 序。

对三个元素 \(u,v,w\) ,满足 \(\operatorname{dfn}(u)<\operatorname{dfn}(v)<\operatorname{dfn}(w)\),有 \(\operatorname{lca}(w,u,v)=\operatorname{lca}(u,w)\)

\(\text{Proof 2.1}\)

由 Theorem 1.2,\(\operatorname{lca}(u,v,w)\in\{\operatorname{lca}(u,v),\operatorname{lca}(u,w)\}\)

假设 \(\operatorname{lca}(u,v,w)=\operatorname{lca}(u,v)\) 并且 \(\operatorname{lca}(u,v)\neq \operatorname{lca}(u,w)\)

\(x=\operatorname{lca}(u,v,w)\)

因此 \(v\)\(\operatorname{lca}(u,w)\)\(x\) 的不同子树。又有 \(\operatorname{dfn}(v)>\operatorname{dfn}(u)\),所以 \(u\) 所在的 \(x\) 的子树先被访问,即 \(\operatorname{lca}(u,w)\) 所在的 \(x\) 的子树先被访问,因此 \(\operatorname{dfn}(w)<\operatorname{dfn}(v)\)。矛盾,故假设不成立。

证毕。

\(\text{Theorem 2.2}\)

对于序列 \(a\),若集合 \(A=\{a_1,a_2\cdots a_{n}\}\),且满足 \(\forall i\in [1,n),\operatorname{dfn}(a_{i+1})>\operatorname{dfn}(a_i)\),有 \(\operatorname{lca}(a_1,a_n)=\operatorname{lca}(A)\)

\(\text{Proof 2.2}\)

归纳证明,设对 \(k=n\) 成立。

由 Theorem 1.2, \(\operatorname{lca}(A\cup \{a_{n+1}\})=\operatorname{lca}(a_{n+1},\operatorname{lca}(A)\})\)

由 Lemma 2.1,Theorem 1.2,\(\operatorname{lca}(\operatorname{lca}(A),a_{n+1})\operatorname{lca}(\operatorname{lca}(a_1,a_{n}),a_{n+1})=\operatorname{lca}(a_1,a_{n},a_{n+1})=\operatorname{lca}(a_1,a_{n+1})\)

因此定理对 \(k=n+1\) 同样成立。

证毕。

\(\text{Theorem 2.3}\)

对于序列 \(a\),若集合 \(A=\{a_1,a_2\cdots a_{n}\}\),且满足 \(\forall i\in [1,n),\operatorname{dfn}(a_{i+1})>\operatorname{dfn}(a_i)\)

\(\operatorname{lca}(a_1,a_n)\in \bigcup\limits_{i=1}^{n-1}\{\operatorname{lca}(a_i,a_{i+1})\}\)

\(\text{Proof 2.3}\)

由 Theorem 2.1 ,\(\operatorname{lca}(a_1,a_n)=\operatorname{lca}(A)\)

由 Theorem 1.1,\(\operatorname{lca}(A) \in \{\operatorname{lca}(a_i,a_{i+1}),i\in[1,n)\}\)

前情提要

某道题,某人写了,然后交了,然后过了,但跑的很慢。

某人卡了好几次常,都不行。

看了下跑得比较快的代码,其实没啥差别。

输出运行时间,发现没什么差异,所以某人要对程序做个性能分析。

性能分析本质上是运行了一遍程序,然后把占用大量时间的函数抓出来,对这个函数进行优化。

在 Windows 下使用 gprof 对程序进行性能分析

gprof 是 g++ 自带的性能分析工具,简单易用,命令行中 gprof --version 查看是否可用,如果不行,先把 g++ 添加进系统路径里,多半就可以了。

性能分析需要特殊的编译参数,格式为

g++ -pg demo.cpp -o demo

其它参数可以照常添加,但是不能开 O2 等性能优化,这会破坏掉程序结构。

然后运行程序,这一步必不可少。

./demo

不要使用其它IDE编译执行后的gmon.out文件进行性能分析,请在命令行使用以上命令编译运行后进行下面的操作

gprof demo.exe gmon.out > result.txt

这会生成性能数据到 result.txt 中,查看一下就行。

最后一步中,.exe 不能省略

给出一个性能数据的例子,例子中有详细的解释,我就不解释了

啥,看不懂英文?那就别学了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
Flat profile:

Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
17.14 0.06 0.06 3 20.00 74.92 NTT(num*, int)
17.14 0.12 0.06 _mcount_private
14.29 0.17 0.05 14680064 0.00 0.00 num::num(int, int, int)
14.29 0.22 0.05 7602174 0.00 0.00 num::operator*(num const&)
11.43 0.26 0.04 7077888 0.00 0.00 num::operator-(num const&)
11.43 0.30 0.04 __fentry__
5.71 0.32 0.02 7077888 0.00 0.00 num::operator+=(num const&)
2.86 0.33 0.01 392448 0.00 0.00 std::enable_if<std::__and_<std::__not_<std::__is_tuple_like<num> >, std::is_move_constructible<num>, std::is_move_assignable<num> >::value, void>::type std::swap<num>(num&, num&)
2.86 0.34 0.01 262144 0.00 0.00 num::operator*=(num const&)
2.86 0.35 0.01 main
0.00 0.35 0.00 1177344 0.00 0.00 std::remove_reference<num&>::type&& std::move<num&>(num&)
0.00 0.35 0.00 149817 0.00 0.00 void read<int>(int&)
0.00 0.35 0.00 149815 0.00 0.00 num::num(int)
0.00 0.35 0.00 149813 0.00 0.00 printf(char const*, ...)
0.00 0.35 0.00 149813 0.00 0.00 num::get()
0.00 0.35 0.00 16 0.00 0.00 quick(int, int, int, int)

% the percentage of the total running time of the
time program used by this function.

cumulative a running sum of the number of seconds accounted
seconds for by this function and those listed above it.

self the number of seconds accounted for by this
seconds function alone. This is the major sort for this
listing.

calls the number of times this function was invoked, if
this function is profiled, else blank.

self the average number of milliseconds spent in this
ms/call function per call, if this function is profiled,
else blank.

total the average number of milliseconds spent in this
ms/call function and its descendents per call, if this
function is profiled, else blank.

name the name of the function. This is the minor sort
for this listing. The index shows the location of
the function in the gprof listing. If the index is
in parenthesis it shows where it would appear in
the gprof listing if it were to be printed.

Copyright (C) 2012-2018 Free Software Foundation, Inc.

Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved.

Call graph (explanation follows)


granularity: each sample hit covers 4 byte(s) for 2.86% of 0.35 seconds

index % time self children called name
<spontaneous>
[1] 71.4 0.01 0.24 main [1]
0.06 0.16 3/3 NTT(num*, int) [2]
0.01 0.00 262144/262144 num::operator*=(num const&) [10]
0.00 0.00 524286/7602174 num::operator*(num const&) [3]
0.00 0.00 2/14680064 num::num(int, int, int) [6]
0.00 0.00 149817/149817 void read<int>(int&) [96]
0.00 0.00 149815/149815 num::num(int) [97]
0.00 0.00 149813/149813 num::get() [99]
0.00 0.00 149813/149813 printf(char const*, ...) [98]
0.00 0.00 16/16 quick(int, int, int, int) [100]
-----------------------------------------------
0.06 0.16 3/3 main [1]
[2] 64.2 0.06 0.16 3 NTT(num*, int) [2]
0.05 0.02 7077888/7602174 num::operator*(num const&) [3]
0.04 0.02 7077888/7077888 num::operator-(num const&) [4]
0.02 0.00 7077888/7077888 num::operator+=(num const&) [8]
0.01 0.00 392448/392448 std::enable_if<std::__and_<std::__not_<std::__is_tuple_like<num> >, std::is_move_constructible<num>, std::is_move_assignable<num> >::value, void>::type std::swap<num>(num&, num&) [9]
-----------------------------------------------
0.00 0.00 524286/7602174 main [1]
0.05 0.02 7077888/7602174 NTT(num*, int) [2]
[3] 21.7 0.05 0.03 7602174 num::operator*(num const&) [3]
0.03 0.00 7602174/14680064 num::num(int, int, int) [6]
-----------------------------------------------
0.04 0.02 7077888/7077888 NTT(num*, int) [2]
[4] 18.3 0.04 0.02 7077888 num::operator-(num const&) [4]
0.02 0.00 7077888/14680064 num::num(int, int, int) [6]
-----------------------------------------------
<spontaneous>
[5] 17.1 0.06 0.00 _mcount_private [5]
-----------------------------------------------
0.00 0.00 2/14680064 main [1]
0.02 0.00 7077888/14680064 num::operator-(num const&) [4]
0.03 0.00 7602174/14680064 num::operator*(num const&) [3]
[6] 14.3 0.05 0.00 14680064 num::num(int, int, int) [6]
-----------------------------------------------
<spontaneous>
[7] 11.4 0.04 0.00 __fentry__ [7]
-----------------------------------------------
0.02 0.00 7077888/7077888 NTT(num*, int) [2]
[8] 5.7 0.02 0.00 7077888 num::operator+=(num const&) [8]
-----------------------------------------------
0.01 0.00 392448/392448 NTT(num*, int) [2]
[9] 2.9 0.01 0.00 392448 std::enable_if<std::__and_<std::__not_<std::__is_tuple_like<num> >, std::is_move_constructible<num>, std::is_move_assignable<num> >::value, void>::type std::swap<num>(num&, num&) [9]
0.00 0.00 1177344/1177344 std::remove_reference<num&>::type&& std::move<num&>(num&) [95]
-----------------------------------------------
0.01 0.00 262144/262144 main [1]
[10] 2.9 0.01 0.00 262144 num::operator*=(num const&) [10]
-----------------------------------------------
0.00 0.00 1177344/1177344 std::enable_if<std::__and_<std::__not_<std::__is_tuple_like<num> >, std::is_move_constructible<num>, std::is_move_assignable<num> >::value, void>::type std::swap<num>(num&, num&) [9]
[95] 0.0 0.00 0.00 1177344 std::remove_reference<num&>::type&& std::move<num&>(num&) [95]
-----------------------------------------------
0.00 0.00 149817/149817 main [1]
[96] 0.0 0.00 0.00 149817 void read<int>(int&) [96]
-----------------------------------------------
0.00 0.00 149815/149815 main [1]
[97] 0.0 0.00 0.00 149815 num::num(int) [97]
-----------------------------------------------
0.00 0.00 149813/149813 main [1]
[98] 0.0 0.00 0.00 149813 printf(char const*, ...) [98]
-----------------------------------------------
0.00 0.00 149813/149813 main [1]
[99] 0.0 0.00 0.00 149813 num::get() [99]
-----------------------------------------------
0.00 0.00 16/16 main [1]
[100] 0.0 0.00 0.00 16 quick(int, int, int, int) [100]
-----------------------------------------------

This table describes the call tree of the program, and was sorted by
the total amount of time spent in each function and its children.

Each entry in this table consists of several lines. The line with the
index number at the left hand margin lists the current function.
The lines above it list the functions that called this function,
and the lines below it list the functions this one called.
This line lists:
index A unique number given to each element of the table.
Index numbers are sorted numerically.
The index number is printed next to every function name so
it is easier to look up where the function is in the table.

% time This is the percentage of the `total' time that was spent
in this function and its children. Note that due to
different viewpoints, functions excluded by options, etc,
these numbers will NOT add up to 100%.

self This is the total amount of time spent in this function.

children This is the total amount of time propagated into this
function by its children.

called This is the number of times the function was called.
If the function called itself recursively, the number
only includes non-recursive calls, and is followed by
a `+' and the number of recursive calls.

name The name of the current function. The index number is
printed after it. If the function is a member of a
cycle, the cycle number is printed between the
function's name and the index number.


For the function's parents, the fields have the following meanings:

self This is the amount of time that was propagated directly
from the function into this parent.

children This is the amount of time that was propagated from
the function's children into this parent.

called This is the number of times this parent called the
function `/' the total number of times the function
was called. Recursive calls to the function are not
included in the number after the `/'.

name This is the name of the parent. The parent's index
number is printed after it. If the parent is a
member of a cycle, the cycle number is printed between
the name and the index number.

If the parents of the function cannot be determined, the word
`<spontaneous>' is printed in the `name' field, and all the other
fields are blank.

For the function's children, the fields have the following meanings:

self This is the amount of time that was propagated directly
from the child into the function.

children This is the amount of time that was propagated from the
child's children to the function.

called This is the number of times the function called
this child `/' the total number of times the child
was called. Recursive calls by the child are not
listed in the number after the `/'.

name This is the name of the child. The child's index
number is printed after it. If the child is a
member of a cycle, the cycle number is printed
between the name and the index number.

If there are any cycles (circles) in the call graph, there is an
entry for the cycle-as-a-whole. This entry shows who called the
cycle (as parents) and the members of the cycle (as children.)
The `+' recursive calls entry shows the number of function calls that
were internal to the cycle, and the calls entry for each member shows,
for that member, how many times it was called from other members of
the cycle.

Copyright (C) 2012-2018 Free Software Foundation, Inc.

Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved.

Index by function name

[2] NTT(num*, int) [6] num::num(int, int, int) [9] std::enable_if<std::__and_<std::__not_<std::__is_tuple_like<num> >, std::is_move_constructible<num>, std::is_move_assignable<num> >::value, void>::type std::swap<num>(num&, num&)
[96] void read<int>(int&) [10] num::operator*=(num const&) [7] __fentry__
[100] quick(int, int, int, int) [4] num::operator-(num const&) [5] _mcount_private
[98] printf(char const*, ...) [3] num::operator*(num const&) [1] main
[99] num::get() [8] num::operator+=(num const&)
[97] num::num(int) [95] std::remove_reference<num&>::type&& std::move<num&>(num&)

VTune

一个更厉害的性能分析工具,可以分析 CPU 流水线利用率和高速缓存命中率等进一步性能。

ARC144D

题意略。

观察,发现 \(x \& y + x |y = x+y\),原因是每个二进制位,如果出现两次,会计入二次,如果出现一次,会计入一次。猜想 \(f(x)\) 的值与每个二进制位相关且对于每个二进制位独立。

考虑证明,对 \(x\) 的二进制下 \(1\) 的个数 \(n\) 做归纳。

对于 \(n=0\)\(f(0)=f(0)\)

对于 \(n=1\)\(f(2^i) = f(2^i)\)

对于 \(n=2\)\(f(2^i+2^j) + f(0) = f(2^i) +f(2^j)\)

考虑证明 \(n=k\) 时, \(f(x) +(k-1)f(0) = \sum\limits_{i\in x} f(2^i)\)

设对于 \(n=k-1\) 其成立,对于二进制位 1 个数为 k 的 x, \(\forall i\in x,f(x)+f(0)=f(x-2^i) + f(2^i)\),易证 \(f(x) +(k-1)f(0) = \sum\limits_{i\in x} f(2^i)\),且对于所有涉及元素二进制下 1 个数少于 n 的,所有限制满足。

故只需要确定 \(f(2^i)\)\(f(0)\)。其最终限制有两个,其一是,最大值不能超过 \(k\),其二是最小值不能小于 0,最大值一定是把所有大于 \(f(0)\) 的元素加起来,最小值则是把所有小于 \(f(0)\) 的元素加起来。

枚举 \(f(0)\) 和小于 \(f(0)\) 的元素个数,我们可以将限制分配给对应元素 ,得到和式: \[ ans=\sum\limits_{f(0)=0}^{k}\sum\limits_{i=0}^{n}\binom{n}{i}\binom{f(0)}{i}\binom{k-f(0)+n-i}{n-i} \] 第一项计算那些小于 \(f(0)\),第二项是将至多 \(k\) 个冗余值分配给 \(i\) 个小于 \(f(0)\) 的元素的方案数,每个元素至少分配一个,或者说 \(i\) 个正整数变量和为 \(f(0)\) 的方案,可以给一个虚拟元素分别大于等于 0 个表示放弃,所以先给虚拟元素一个,变成 \(f(0)+1\) 个元素插 \(i\) 个板。最后一项是 \(n-i\) 个非负整数变量,和不超过 \(k-f(0)\) 的方案。先来个虚拟变量,然后先各自加 \(1\),然后就是 \(k-f(0)+n-i+1\) ,间隔是 \(k-f(0)+n-i\) 个,版一共 \(n\) 个。

解释完了,考虑计算,二维的,算不了。

考虑后两项的组合意义。

根据组合恒等式中的第五个。

对原式变形有 \[ ans=\sum\limits_{i=0}^{n}\binom{n}{i}\binom{k-f(0)+n-i+1}{n-i+1} \] 然后 \(n\) 不大,维护个区间乘积枚举计算就行,记得对计算乘积的元素取模。

组合恒等式

题不是终点,组合恒等式才是重点

对上面那种图的组合恒等式,我们一一证明

式子1

\[ \sum\limits_{k}\binom{r}{m+k}\binom{s}{n-k}=\binom{r+s}{m+n} \]

组合意义证明

第一项是从 \(r\) 个选 \(m+k\) 个,第二项是从 \(s\) 个选 \(n-k\) 个,把两个拼在一起,从 \(r+s\) 个中选 \(n+m\) 个,容易发现和前者一一对应,故恒等。

代数证明

咕咕咕

式子2

\[ \sum\limits_{k}\binom{l}{m+k}\binom{s}{n+k} = \binom{l+s}{l-m+n},l\ge0 \]

组合意义证明

代数证明

咕咕咕

式子3

T1

T1

某个不太正确的方式

想到了分块,暴力没操过去。

考虑暴力计算出前 \(\sqrt P\) 个数的值,期望得到的最大值为 \(P-\sqrt P\),剩下来的数一定不多,我的想法是分块探测一下下一个块中是否存在比当前最大值更大的数,探测方式是用个 \(\text{hash\_table}\) 记录下每块的每个数的方幂,然后把等式左边乘上逆元后再查找,但是这个做法只有 \(60pts\),虽然我本机能跑过。

后面想了一下还看了下代码,这个方式的复杂度是错的,因为探测总次数还是不对头。2022.10.7

正常的考虑方式

发现我们对同一个数的探测次数有点多,考虑能不能对一个数只探测一次。本质上是需要求 \(P-1,P-2,\cdots P - \sqrt P\) 这些数在数列中出现的第一个位置。我们的探测实际上是判断了一个幂函数的解是否存在于一个区间。所以直接考虑求解这个幂函数方程 \(x^r = a\)

考虑 \(BSGS\),但是需要解 \(T\times \sqrt{P}\) 次,每次的复杂度为 \(O(\sqrt P)\),不如暴力。

考虑平衡复杂度,设暴力处理的长度为 \(S\)\(BSGS\) 预处理长度为 \(B\),那么复杂度为 $T(S+ + B) $

后面那一坨最优应该是 \(3e6\) ,压力山大。

发现质数是同一个质数,有没有什么办法可以优化一下。

答案是,处理离散对数。

离散对数有着和正常对数一样优美的换底公式,如果我们对 \(x,P-1,P-2\cdots P-\sqrt P\)。向 \(P\) 的原根 \(g\),取对数,那么可以利用换底公式很方便的求解 \(x^r = a\),取对数的过程就是 \(BSGS\),注意到换底公式的除法变成了模 \(P-1\) 意义下的除法,因为 \(g^{P-1} = 1\)

所以 \(BSGS\) 的总次数变为了 \(O(\frac{P}{S})\) 次,预处理只需要一次。这里可以放开玩。

总复杂度变为了 \(O(T*(S+\frac{P}{S})+ B + \frac{P}{S}\times\frac{P}{B})\)

\(B = P^{\frac{3}{4}}\)\(S=\sqrt P\) 可以取到最优。

我实现的时候写的比较丑,算了每个东西的位置之后,还需要排个序,实际上从大到小倒着做就可以了。

看看佳老师的代码,长进不少。

T2

T2

定义字符串的 border 为一个它的前缀,这个前缀也是它的后缀。

场上是只会 \(O(n^2)\) 的,还 TM 暴力打挂了只有 40。

后面了解到可以用 FFT 处理带通配符的字符串匹配问题。

具体的,定义距离函数 \(d_i=\sum\limits_{j=1}^{m} s_{i+j-1}t_i(s_{i+j-1}-t_j)^2\)

显然,如果距离 \(d_i\)\(0\),那么 \(i\) 开头的字符串就能匹配上。

下标有点问题,所以把 \(t\) 反转一下,把式子拆了,就是三遍 FFT 求出来每一个 \(d_i\)

我们就知道了哪些结束位置可能可以匹配上。

不是所有能匹配上的位置一定可以对答案贡献,因为如果前面的匹配上了,通配符就没了,所以后面要匹配上,就要求重叠的部分必须是一个 border。

其实 border 在字符串随机的情况下是很少的。我们可以考虑记录下所有的 border,然后 dp,设 dp[i] 表示强制 i 结尾匹配上了,最多能匹配的个数,如果 \(j\leq i-m\),就是个前缀 max,如果 \(j> i-m\),就只有 border 能转移。

border 有个性质,就是它们的长度构成 \(\log n\) 个等差数列,所以我们考虑对每个等差数列做转移。

更具体的,如果 \(next[n]>\frac{n}{2}\),那么模板串一定是一个循环,并且最小循环长度为 \(n-next[n]\),它的长度大于等于 \(r=n\%(n-next[n])\) 的 border 一定构成一个公差为 \(d=n-next[n]\) 的等差数列。这样的话,\(n\) 每次至少减半,所以至多有 \(\log n\) 个等差数列。

考虑证明。如果存在一个长度大于 \(r\) 的 border \(x\) 长度不能写成 \(kd+r\),感性理解下,把原串划分为长度为 \(d\) 的小串,把 \(x\) 在末尾的部分平移到开头,显然它还有一个更小的循环。

所以我们对于一个点,含 border 的转移就可以分为 \(\log n\) 类,对每一类记一个前缀 \(\max\) 即可转移,注意每一类需要按照模公差分类。

注意转移细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include<bits/stdc++.h>
#define y1 y3647
#define INF 1000000000
#define LL long long
#define pii pair<int,int>
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
const int N=2e5+10;
const double Pi=acos(-1.0);
struct comple{
double x,y;
comple operator +(const comple &a){return comple{x+a.x,y+a.y};}
comple operator -(const comple &a){return comple{x-a.x,y-a.y};}
comple operator *(const comple &a){return comple{x*a.x-y*a.y,x*a.y+y*a.x};}
void operator +=(const comple &a){x+=a.x,y+=a.y;}
void operator -=(const comple &a){x-=a.x,y-=a.y;}
void operator *=(const comple &a){double tp=x;x=x*a.x-y*a.y,y=tp*a.y+y*a.x;}
};
int i,j,k,n,s,t,m,lim,ans;
int res[N],vala[N],valb[N],sum[N],nxt[N];
int dp[N],border[N][2],pre[32][N][2],from[N];
char ch[N],str[N];
double d[N];
comple a[N],b[N],c[N];
void FFT(comple val[],int type=1){
for(i=0;i<1<<lim;i++)if(res[i]>i)swap(val[res[i]],val[i]);
for(i=1;i<=lim;i++){
for(j=0;j<1<<lim;j+=1<<i){
comple w={1,0},wn={cos(2*Pi/(1<<i)),type*sin(2*Pi/(1<<i))};
for(k=0;k<1<<i-1;k++,w*=wn){
comple y=val[k+j+(1<<i-1)]*w;
val[k+j+(1<<i-1)]=val[k+j]-y,val[k+j]+=y;
}
}
}
if(type==-1)for(i=0;i<1<<lim;i++)c[i].x/=(1<<lim);
}
void get_border(int x){
if(x==0)return ;
border[++s][0]=nxt[x],border[s][1]=x-nxt[x];
if(nxt[x]*2>x)get_border(x%(x-nxt[x]));
else get_border(nxt[x]);
}
void print(int pos,int lst){
if(dp[pos]==0)return ;
for(i=1,j=pos-m+1;i<=m&&j<lst;i++,j++){
if(ch[j]!='?'&&ch[j]!=str[i]){
printf("Error");
return ;
}
if(j<1)continue;
ch[j]=str[i];
}
print(from[pos],pos-m+1);
}
signed main()
{
freopen("match.in","r",stdin);
freopen("match.out","w",stdout);
//freopen(".in","w",stdout);
scanf("%s%s",ch+1,str+1);
n=strlen(ch+1),m=strlen(str+1);
nxt[0]=j=-1;
for(i=1;i<=m;i++){
while(j!=-1&&str[j+1]!=str[i])j=nxt[j];
nxt[i]=++j;
}
get_border(m);border[s][1]=1;
while(1<<lim<=n+m)lim++;
for(i=1;i<1<<lim;i++)res[i]=res[i>>1]>>1|(i&1)<<lim-1;
for(i=1;i<=n;i++)vala[i]=ch[i]=='?'?0:ch[i]-'a'+1,sum[i]=sum[i-1]+vala[i]*vala[i]*vala[i];
for(i=1;i<=m;i++)valb[i]=str[i]-'a'+1;

for(i=1;i<=n;i++)a[i]={pow(vala[i],2)};
for(i=1;i<=m;i++)b[i]={-2*valb[m-i+1]};
FFT(a),FFT(b);
for(i=0;i<1<<lim;i++)c[i]+=a[i]*b[i];
memset(a,0,sizeof(a)),memset(b,0,sizeof(b));
for(i=1;i<=n;i++)a[i]={vala[i]};
for(i=1;i<=m;i++)b[i]={pow(valb[m-i+1],2)};
FFT(a),FFT(b);
for(i=0;i<1<<lim;i++)c[i]+=a[i]*b[i];
FFT(c,-1);
for(i=m;i<=n;i++)
d[i]=sum[i]-sum[i-m]+c[i+1].x;

for(i=m;i<=n;i++){
for(j=1;j<=s;j++){
if(pre[j][i-m+border[j][0]][0]+1>dp[i]){
dp[i]=pre[j][i-m+border[j][0]][0]+1;
from[i]=pre[j][i-m+border[j][0]][1];
}
}
if(abs(d[i])>0.1)dp[i]=0;
for(j=1;j<=s;j++){
pre[j][i][0]=dp[i],pre[j][i][1]=i;
if(pre[j][i-border[j][1]][0]>pre[j][i][0]){

pre[j][i][0]=pre[j][i-border[j][1]][0];
pre[j][i][1]=pre[j][i-border[j][1]][1];
}
}
if(dp[i]>dp[ans])ans=i;
}
cout<<dp[ans]<<endl;
print(ans,ans+1);
for(i=1;i<=n;i++)if(ch[i]=='?')ch[i]='a';
cout<<ch+1<<endl;
return 0;
}


pip 和包

网络问题

GFW 会墙掉默认的 pip 源, 所以请使用国内源.

不建议使用代理连接国外源.

使用国内源时一些注意事项:

  • 彻底关闭代理软件(退出软件, 关 System Proxy 没用), 并检查系统代理设置, 确认没有设置代理端口. 由它引起的报错:

    • SSL 错误(报错大概长下面这样), 国内源不允许代理连接, 用的 TLS 方式, 代理的 SSL 证书通不过验证.

    1
    WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1000)'))': /simple/proxyscrape/

    • Proxy 连接错误(报错大概长下面这样). 原因是命令行代理没有配置好, 但是 pip 检测到了系统有代理尝试使用.

    1
    WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', timeout('_ssl.c:1091: The handshake operation timed out'))': /simple/pandas/

  • 清华源同步很慢, 速度不稳定, 建议使用阿里云源, 命令行执行(永久设置):

    1
    2
    pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
    pip config set global.trusted-host mirrors.aliyun.com

  • 检查全局源设置:

    1
    pip config list

    输出应该是:

    1
    2
    global.index-url='https://mirrors.aliyun.com/pypi/simple'
    install.trusted-host='mirrors.aliyun.com'

虚拟环境

Python 包各种包的有复杂的版本依赖关系, 把所有包装在一起不可取.

建议安装个 anaconda 创建虚拟环境, 然后在虚拟环境里安装包.

anaconda 安装教程.

额外提几个注意:

  • 安装 conda 前, 应该彻底卸载之前安装的 Python, 直接用卸载功能不会删除 pip 相关的包, 正确的卸载方式:

    1. 找到并记住安装路径.

    2. 用卸载程序(就是安装包)卸载.

    3. 彻底删除安装路径.

  • 第一次安装 conda 完成后, 必须在终端执行 conda init 命令使 conda 生效.

局部环境

虚拟环境本质上就是建立了若干个局部环境, 并使用一个全局的管理工具来管理这些局部环境.

所以也可以直接建立局部环境, Python 的标准库有个 venv, 就是用局部环境来做虚拟环境的.

局部环境也可以用安装包安装, 不要勾选添加系统路径, 然后自定义安装路径就能安装 Windows 的 Python 局部环境.

Linux 下安装 Python 局部环境

用局部环境的路径去运行 pip, Python 时, 所有操作都会在这个局部环境里执行, 与全局环境无关.

理论上这个东西在制作傻瓜式软件的时候有一定作用, 但根据我的经验, 虽然可能全国有 1% 的人会 Python, 但全国只有 0.1% 的人能够正确安装小众的 Python 第三方库, 所以还是很有用的.

上周见到两个 conda 装了不 init 的胎神.

import

name

__name__ 是 Python 的一个内置变量, 用于判断当前文件是否被直接运行.

python xxx.py 直接运行, __name__ 为 "main".

python -m xxx 间接运行, __name__模块名.

import 包时的执行逻辑

用 import 或者 from 导入模块时, 对应模块代码会被原封不动的全部执行一遍, 执行时, 被 import 的代码中 __name__模块名.

import 寻找顺序

有一个 sys.path 变量, 里面存着一些路径, 用于寻找模块. 这玩意是个列表, 从前往后依次寻找, 找到就开始导入, 顺序大概这样: 1. 在工作目录下寻找(绝对或相对导入). 2. 在 site-packages 目录下寻找(仅绝对导入).

莫名其妙的 id 不一致问题

有时候, 你手动加了 sys.path 后, 导入时全局变量可能会出现 id 不一致的问题, 讲下原因:

  • ma 模块假设在 module/ma.py 里.

  • a.py 代码用 module.ma 导入了 ma 模块的一个全局变量 g.

  • sys.path 里加了 module 这个目录.

  • module/b.py 代码中用 ma 导入了 ma 模块的全局变量 g.

  • a.pyb.pyg 的 id 不一致.

所以别问为啥全局变量修改无效了.

从不同目录下(sys.path值不同)导入同一个文件时, 被导入的模块代码有不同的 id, 所以里面的东西也有两份.

无特殊需求, 只建议项目使用绝对导入.

import 的代码到底执行了几遍

用同一个 sys.path 量导入的模块代码只会执行一遍, 第一次执行后被加进 cache 里了, 第二次 import 时发现 cache 里有就直接用 cache 里的了, 不执行第二次.

PYPI

PYPI 打包发布的时候, 如果文件夹没有 __init__.py 文件, 那么这个文件夹会扔掉打包不进去.

关于 Request 这一类网络请求函数

Requests 库的 POST 相关

  • requests 的 post 方法有点坑,目前主流数据提交方式都是 json,所以一般用 json 这个可选参数传参,data 传参可能会因为格式问题导致请求失败。
  • 需要显式指定协议,直接写 example.com 会报错。
1
2
3
4
5
6
import requests

requests.post("http://example.com", json={
"token": "xxx",
"value": "yyy"
})

Request 爬虫的时代已经过去了, 现在用 selenium 吧.

当然如果直接最简单的 Request 没出问题也不妨试试.

设置 pip 默认下载源

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

参考

环境

基础

  • git
  • nodejs && npm

开工作目录

1
2
3
4
5
npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo server

工作目录blog,记住,这很重要,文件名默认为相对工作目录的名字。

本地预览

工作目录下:

1
hexo g && hexo s

localhost:4000 上看。

开 GitHub Pages 仓库

每个账号只能开一个,仓库名 username.github.io

配源码分支

创仓库的时候把 main 弄下来,源码(不含 public)放这里,免得被覆盖。

自动部署

工作目录下安自动部署工具。

1
npm install hexo-deployer-git --save

_config.yml 末尾加上(记得照着用户名改动):

1
2
3
4
5
deploy:
type: git
repo: https://github.com/username/username.github.io.git
branch: master
# message: Site updated: {{ now('YYYY-MM-DD HH:mm:ss') }})

部署:

1
hexo clean && hexo g && hexo d

metadata

_config.yml 相关配置项:

1
2
3
4
5
6
7
8
title: 幻影彭的彩虹
subtitle: '记录青春的扇区'
description: ''
keywords: '算法竞赛, 自动化测试, 工程技术, Python'
author: huan-yp
language: zh-CN # 必须改

url: https://huanyp.cn

主题

安主题(next)

工作目录下:

1
git clone https://github.com/next-theme/hexo-theme-next themes/next

写上要用这个主题

_config.yml 里,找到 theme,写上 next

next

我用 next 主题。

配置菜单。

1
2
3
4
5
6
7
8
9
menu:
home: / || fa fa-home
about: /about/ || fa fa-user
tags: /tags/ || fa fa-tags
categories: /categories/ || fa fa-th
archives: /archives/ || fa fa-archive
# schedule: /schedule/ || fa fa-calendar
sitemap: /sitemap.xml || fa fa-sitemap
commonweal: /404/ || fa fa-heartbeat

scheme

themes/next/_config.yml 调 scheme,传统的应该选 Gemini

1
2
3
4
5
# Schemes
# scheme: Muse
# scheme: Mist
# scheme: Pisces
scheme: Gemini

dark

应该没有人喜欢 light,所以调成 dark。

themes/next/_config.yml 调 darkmode。

1
darkmode: true

背景

一般要加个背景图片什么的

准备 source 图片

图片放到 themes/next/source/image/background.jpg 里。

改 style

找到 themes\next\source\css\_schemes\Gemini\index.styl,加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
body {
background:url(/images/background-now.jpg);
background-repeat: no-repeat;
background-attachment:fixed; //不重复
background-size: cover; //填充
background-position:50% 50%;
}


// 博客内容透明化
// 文章内容的透明度设置
.content-wrap {
opacity: 0.7;
}


// 侧边框的透明度设置
.sidebar {
opacity: 0.7;
}

// 菜单栏的透明度设置
.header-inner {
background: rgba(255, 255, 255, 0.8);
}

// 搜索框(local-search)的透明度设置
.popup {
opacity: 0.7;
}

//博客内容透明化

:root {
--content-bg-color: rgba(255, 255, 255, 0.8);
}

那个 [0, 1] 的参数是透明度,自己估摸着调。

友情链接

1
2
menu:
友情链接: /links/ || fa fa-link

写中文就行。

内联 html

新建 source/links/index.md,写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{% raw %}
<div class="post-body">
<div id="links">
<style>
.links-content{
margin-top:1rem;
}
.link-navigation::after {
content: " ";
display: block;
clear: both;
}
.card {
width: 45%;
font-size: 1rem;
padding: 10px 20px;
border-radius: 4px;
transition-duration: 0.15s;
margin-bottom: 1rem;
display:flex;
}
.card:nth-child(odd) {
float: left;
}
.card:nth-child(even) {
float: right;
}
.card:hover {
transform: scale(1.1);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
}
.card a {
border:none;
}
.card .ava {
width: 3rem!important;
height: 3rem!important;
margin:0!important;
margin-right: 1em!important;
border-radius:4px;
}
.card .card-header {
font-style: italic;
overflow: hidden;
width: 100%;
}
.card .card-header a {
font-style: normal;
color: #2bbc8a;
font-weight: bold;
text-decoration: none;
}
.card .card-header a:hover {
color: #d480aa;
text-decoration: none;
}
.card .card-header .info {
font-style:normal;
color:#a3a3a3;
font-size:14px;
min-width: 0;
overflow: hidden;
white-space: nowrap;
}
</style>
<div class="links-content">
<div class="link-navigation">
<div class="card">
<img class="ava" src="博客图标" />
<div class="card-header">
<div>
<a href="博客链接">博客名字</a>
</div>
<div class="info">博客简介</div>
</div>
</div>
<div class="card">
<img class="ava" src="博客图标" />
<div class="card-header">
<div>
<a href="博客链接">博客名字</a>
</div>
<div class="info">博客简介</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endraw %}

要加的时候在这复制 html 就行。

杂项

置顶文章

工作目录下执行:

1
2
npm uninstall --save hexo-generator-index
npm install --save hexo-generator-index-pin-top

metadata 部分写:

1
2
3
4
---
title: Title
top: true
---

即可置顶。

折叠

metadata 中加上:

1
2
3
---
description: 文件摘要
---

即可折叠.

插件

搜索插件

工作目录下:

1
npm install hexo-generator-searchdb

主题配置 themes/next/_config.yml 下:

1
2
3
4
5
6
7
8
local_search:
enable: true
# Show top n results per article, show all results by setting to -1
top_n_per_article: 2
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false

有时候会出现部分文章能搜索,部分文章不能搜索的情况。这一般是因为 search.xml 中有不合法的字符导致 xml 解析失败引起的。浏览器访问一下你的 xml 文件,会有报错提示你在哪里。

数学插件

一般用 Mathjax。

先安装 Pandoc,这个要改系统路径,需要重启终端。

工作目录下执行:

1
2
npm uninstall hexo-renderer-marked
npm install hexo-renderer-pandoc

themes/next/_config.yml 的 math 配置:

1
2
3
4
5
math:
mathjax:
enable: true
# Available values: none | ams | all
tags: all

每篇文档的 metadata 部分要写启用 Mathjax:

1
2
3
4
---
title: Hello World
mathjax: true
---

参考资料:Next 官方有关文档

常用命令

  • hexo g : 生成
  • hexo s : 本地部署
  • hexo d : 远端部署

网络相关

CNAME

hexo d 的时候因为是强制 push 的, github CNAME 文件时会被覆盖, 导致域名解析错误.

解决方式

CNAME 放入 ./source,即存在 ./source/CNAME 文件。

SEO

google search console 申请抓取的时候,一定要看清楚是不是 https 协议,如果部署在 github 上,可能会强制 https,导致抓取出现重定向错误。如果出现了这个错误,有可能是没用 https 协议。

0%