0%

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\times (S+\frac{P^2}{S\times B} + 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 和包

  • 用 pip 安装包时,windows 会出各种各样的问题,请关闭代理并使用国内源安装
  • 将所有包放在系统 python 中是一个坏习惯,python 各种包的有复杂的版本依赖关系,请使用 anaconda 或者 virtualenv 等包管理器。
  • 如果你没有 install 一个包,那么只能在包的父目录下直接 import 它,其它位置不能引入,如果 install 了,那么你在该环境中任何位置都可以用了,相当于加入了 sys.path

airtest

import

正常情况下,一个项目中,同一文件被引用两次,那么只会在第一次执行。

但是如果你乱改 sys.path,然后在 __init__.py 里直接用 import xxx 引入,然后再在外面乱引入,就有可能被引入多次。

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

Python 的 Requests 包对请求做了一些包装,但是 NodeJS的 Request 没有这种包装。

因此可能会引发一些 403 问题(比如 Python 的 Requests 可以正常用但 NodeJS 的 Request 不行),可以输出一下它们的 headers 看看最终发出的请求长什么样子。

这是 Python。

1
2
3
4
5
6
{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate',
'Accept': '*/*',
'Connection': 'keep-alive'
}

这是 JS。

1
2
3
{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
}

headers:

1
2
3
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
}

nodejs 的 request 由于太过常用,已经被一些服务器加入了黑名单,方式是通过建立 https 时的一些 ciphers 来判别是否属于爬虫,容易发生 403。

参考资料

关于 Clash

Python

用 Clash-For-Windows 的代理的时候,有时候会报 OpenSSL 1131 错误,一般来说配置一下请求的代理到 localhost:7890 就行,注意好像是不支持 HTTPS 代理的,HTTPS 代理填 http://127.0.0.1:7890

比较靠谱的一种方式:

1
2
os.environ['http_proxy'] = f"{proxy['host']}:{proxy['port']}"
os.environ['https_proxy'] = f"{proxy['host']}:{proxy['port']}"

设置 pip 默认下载源

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

Hexo-NexT 安装踩坑记录

背景图片设置

.\themes\next\_config.yml 设置好 custom_file_pathstyle 属性后,那个路径里面的 /source/_data/styles.styl 要建在根目录的 source 下而不是 .\themes\next 目录下的

CNAME 文件部署时被覆盖

解决方式

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

SEO

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

CF1698F

思路

变过来变过去,还 TM 离谱的操作当然要找不变量,算是半个套路。

reverse 两端相等的区间,可以发现每两个相邻元素构成的无序对不变,然后第一个元素和最后一个元素不改变。

换句话说,$\{(a_i,a_{i+1},i \in [1,n)\}$ 这个集合不随 $a$ 中操作改变。或者说,如果从 $a_i$ 到 $a_{i+1}$ 连边,这张无向图是不变的,同时 $a_1,a_n$ 不改变。这个条件和上个条件之间的转换是解题的关键点之一。

套路又来了,看看样例,集合相同并且首尾相同这个条件貌似挺充分的,所以考虑证明。

对于构造问题,常用的证明方式是数学归纳法。

事实上,如果我们能证明如果第二个数可以成功调整为相同,那么整个数组也可以,因为满足条件的 $a,b$ 如果同时删掉第一个数,仍满足条件。

考虑构造方案,由结论可以知道,如果 $a$ 中必定存在 $(b_1,b_2)$ 无序对,因为 $a_2\neq b_2$,不妨让这对无序对是 $(a_x,a_{x+1}),x\in[2,n)$,如果 $a_x=b_2$,考虑直接将这一对中的 $a_{x+1}$ 和 $a_1$ 子数组 reverse ,完事。如果 $a_{x+1}=b_2$,事情有点麻烦。

if only i could find a pair which...

补上那句话,如果能找到一对可以被翻转的,左端点在 $[1,x]$ ,右端点在 $(x,n]$ 的,那么我们就翻转,改变了 $a_x,a_{x+1}$ 的顺序。

尝试证明一定能找到,即 $\{a_i ,i\in [1,x]\} \bigcap \{a_i.i\in(x,n]\} \neq \emptyset$

反证法,如果找不到,那么可知 $a_i,i\in[1,x] $ 与 $a_j,j\in (x,n]$ 除开 $(a_i,a_i+1)$ 这一次相邻外均不相邻,考虑 $b_2$ 即 $a_{x+1}$ 的情况,它与 $b_3$ 相邻,可知 $b_3 \in \{a_i.i\in(x,n]\}$,同理有 $\forall j\ge 2,b_j\in\{a_i.i\in(x,n]\}$

考虑 $a_2$,它显然不存在于 $b$,故矛盾。

回顾

这题比较难的有两个点,一是注意到这个不变量极大可能是充分条件,二是发现 $a_{x+1}=b_2$ 的 case 中一定存在可交换项,搞定了这两个,问题就迎刃而解。关键点在于第二个,找到 $a_{[1,x]},a_{[x+1,n]}$ 交集的关系,并尝试用反证法证明交集不为空是比较困难的。

ABC259G

很有意思的网络流题。

最开始想到二分图相关,因为 $A_{i,j} <0$ 的限制指向性比较明确,然后发现如果二分图的决策正数话会出现不同块之间相互影响,所以考虑决策负数,决策负数不同联通块互不影响,但是无法计算答案,因为最终还是需要确定到底哪些正数被选了。

解法一

题解给出了一个新思路,考虑先只把所有正数选了,然后再来看满足条件的代价。

代价被分为了三类,第一类是顺带选择的负数的代价,如果选了一行或者一列,就会有这一行或列所有负数绝对值之和的代价。

第二类是无法选择正数的代价,如果正数所在的行和列都没被选择,那么就会有这个正数的代价。

第三类是重复选择负数的代价,如果一个负数被行和列同时选择(为了付出更少的第二类和其它第一类代价),那么这个代价是无穷大。

我们需要最小化代价。

0/1 决策问题,考虑套最小割上去,每一行每一列视为一个点。

令与 $s$ 同集合的为选择,与 $t$ 同集合的为不选,选一行或一列的代价为该行或列负数绝对值之和,从个点到 $t$ 连边就行,行列同时选择负数,代价为 inf,woc,怎么连呢?从行连向列,意义为选了行不选列的代价,从列向行连,意义为选了列不选行的代价。所以我们前面的安排有些问题,需要做出调整。

对于行和列,我们让属于 s,t 所在集合对它们有不同意义,下面让属于 s 的行为不选择,属于 t 的列为不选择,我们让 $s$ 向行连边,这条边表示选该行的代价,让列向 $t$ 连边,表示选该列的代价。于是,对于一个点,行列都选的代价当且仅当 $A_{i,j}<0$ 时为 inf,此时从列向行连边,表示都选的代价,行列都不选的代价当且仅当 $A_{i,j}\ge0$ 时为 $A_{i,j}$,此时从行向列连边。

注意,我们的割中如果出现了行列都不选,那么对应的行和列与 $s,t$ 的边一定没有断开,所以必须断开行到列的边。如果出现了行和列都选的不合法情况,我们发现,断掉的行能到 $t$,从 $s$ 一定能到断掉的列。如果不满足,那么这个割就不是最小割,不会被我们考虑。所以我们需要从列到行连边,保证不会给负数打上两个标记。

这种思路和某类 dp 的思路很类似,相当值得学习,其实在原问题的求解中,并没有什么条件来保证不会给一个负数打上两个标记,但是我们在通过最小割求解时,限定了决策的范围和最优性,获得了额外的信息,也就能帮助我们排除掉难处理但是不可能的情况,本质上,这种排除还和我们先假定所有正数都选上的前提有关系,这种解法相当精妙。

解法二

从直觉上来看,应该不会选择和为负数的行或者列。考虑一个最终方案,它的答案会是 $\sum 选择的行+\sum 选择的列 -\sum 行列交叉处的正数$。考虑从这里面剔除和为负数的列或行,发现最终值一定变大。

所以删掉和为负数的行和列。

考虑先把剩下的行和列全选了,然后解决冲突。

解决冲突的方式有三种,一种时不选行,一种是不选列,另一种是硬吃同时选的代价。

这样的建图就很简洁了,从 $s$ 向行,列向 $t$ 连边,对于交点,正数连其值的边,负数连 inf 边。很容易发现一个合法解和一个割一一对应,over。

Sum up

最小割解决实际问题的核心,在于用一个割,或者可能成为最小割的割,来代表一个实际的决策方案,最小割的容量,代表代价,每个点在哪个割集分别代表什么含义并不重要,重要的是割掉每条边的意义,和决策方案与最小割的对应关系。

其实这道题还给我们一个启发,就是在考虑0/1决策问题时,可以先考虑钦定一个决策,再来调整使得它合理或者变优,这可能会使得问题变得简单,也许算是一个套路。

本质上,最小割表达了一种最优的解决决策冲突的方案,我们在 0/1 决策问题钦定决策的过程中,制造了一些冲突,用一个图的割来表达解决着些冲突的方案。

20220305

ABC242F(二项式反演的进一步理解)

题意:

给定一个 $n*m$ 的矩阵,需要放 $a$ 个白块 $b$ 个黑块,让它们互不侵犯,$n,m\leq 50$。

最开始看到这道题以为又要挑战 $\text{npc}$ 了,仔细一看互不侵犯的定义,是不能放在同一行或同一列。

思考:

很有意思的容斥题,考虑枚举白色占了多大的地盘,设 $f[i][j]$ 表示允许白色占用了 $i$ 行 $j$ 列的方案数,然后组合数容斥。我们可以设 $g[i][j]$ 为白色恰好占用 $i$ 行 $j$ 列的方案数,考虑列出二项式反演的式子。思考 $f,g$ 的关系,这是一类高维容斥问题。

结论如下:

理解这个式子不难,就是减去恰好被占用的部分,注意容斥系数。

我们考虑能不能不用 $g$ 参与这个式子。

回想线性的二项式反演问题,我们有式子:

其中 $f(n)$ 表示钦定满足 $n$ 个条件的方案数。$g(n)$ 表示恰好满足 $n$ 的条件的方案数。

左边很好理解,就是钦定 $n$ 个条件满足的方案数就是从满足 $i$ 个条件的方案中选出 $n$ 个钦定满足。

右边可以稍微变换一下以便理解

就是钦定 $i$ 个的情况减去恰好有大于 $i$ 个的情况乘上一个组合数,这和上面那个二维的很类似。

给出高位二项式反演公式:

第一个二维式子和第二个一维式子很类似,所以考虑推出 $f,g$ 的关系。

组合解释是钦定 $n$ 行 $m$ 列可以被占用的方案数就是先从其中选出 $i$ 行 $j$ 列。然后计算恰好这么多被占用的方案数。 因为行和列是有标号的。

直接代公式有

结束。

ABC242H(初步理解 min-max 容斥)

题意:

给定 $m$ 条线段,一个长度为 $n$ 的数轴,每次随机选一条把数轴上对应位置涂黑,问全部涂黑的期望选择次数。

思考:

记 $E(i)$ 表示第 $i$ 个格子被涂黑的期望时间。记 $E(S)$ 表示集合 $S$ 全部被涂黑的期望时间,也就是所有格子中被涂黑时间的最大值,$E’(S)$ 表示集合 $S$ 中任意一个被涂黑的期望时间,即最小值。

显然 $E(S) \neq \max\limits_{i\in S} (E(i))$,因为期望只有线性性,$max,min$ 是非线性操作。

但是我们发现一件事 $\max(S) = \sum\limits_{T \subseteq S} (-1)^{|T| - 1} \min(T)$,即 $\text{min-max}$ 容斥的公式在期望意义下也成立。

而对这道题来说,计算一个集合的最小值是容易的,只需要计算出有多少个包含任意一个元素的线段就行了。

我们设 $dp[i][j][k][0/1]$ 表示考虑到第 $i$ 个位置,上一个位置为 $j$,已经包含 $k$ 条线段的方案数,集合大小为奇数或偶数的方案数,转移可以预处理 $[l,r]$ 会新增多少线段,做到 $O(1)$,事实上最后一维可以省掉,直接带系数转移就行。

20220309

haltoj128

思考:

欧拉图计数相关问题。关于无向欧拉图有一个结论,欧拉子图的个数为 $2^{m-n+c}$ 个,也就是其生成森林中非树边组成的集合个数,公式中 $c$ 代表连通块个数。

理解比较容易,考虑构造方案,任意一个非树边集合会唯一对应一种合法方案,选一条非树边则将它覆盖的树边状态反转(选变为不选,不选变为选),可以得到唯一合法方案。

这个选非树边集合的方式给这道题目带来了启发。然而这种类似异或的方式并不便于统计 $|S|^2$ 这种东西。我们考虑它的组合意义。发现其组合意义为每对边在同一子图便贡献两次,一条边在某一子图贡献一次。

一条边的情况是简单的,考虑两条边。

两条非树边是可以任选的,这一部分答案为 $k (k-1) 2^{k-2}$,因为已经钦定这两边要选,其它的任意选。

一条非树边和一条树边的贡献可以分两种情况,分别是树边是否受到非树边影响。不受影响答案为 $(k-cover[v]) * 2^{k-2}$,$cover[v]$ 影响这条树边的非树边条数,$k-2$ 因为钦定了选择的非树边要选,并且影响这条树边的边有一个的选择情况是不能任意,因为要让树边被选择。

如果受到非树边影响,那么答案也为 $cover[v] * 2^{k-2}$,但如果 $cover[v] = 1$,那么答案为 $2^{k-1}$。理解方式类似。

两条树边的情况,考虑影响这两条树边的边集,我们断言如果边集完全相同,那么答案为 $2^{k-1}$,否则答案为 $2^{k-2}$,边集完全相同的情况不难理解,如果不完全相同,我们在每条边特有的部分钦定一个来控制该边,所以答案为 $2^{k-2}$。如果是包含关系,先钦定里面的,再钦定外面的即可。

如果要选择树边,记得不要考虑 $cover[v] = 0$ 的边。计算 $cover$ 可以树上前缀和,对于两条非树边,需要统计边集相同的个数,这个可以异或哈希,取 $2^{63}$ 为上界,做双哈希,错误概率在本题数据规模下小于 $10^{-9}$。

haltoj132(分治NTT的思路)

思考:

假设不考虑 $’>’$,即令 $’>’$ 为无限制,那么序列会被 $’>’$ 划分为若干段,记每一段的长度为 $a$,那么答案为

我们考虑容斥,枚举一个子集表示那些位置上的 $’>’$ 强制为 $’<’$,也就是不合法的情况,然后就可以用总数减去这些不合法情况得到答案。

上面的那个 $n!$ 在做转移的时候很麻烦,先不管。

设 $dp[i]$ 表示对前 $i$ 个符号做容斥,考虑到第 $i$ 个符号后的数字的结果。

注意到这里 $dp[i] \times (i+1)!$ 也就是前缀 $s_i$ 的答案。

顺便设 $f_i$ 表示前 $i$ 个符号中 $’>’$ 的个数。

显然有 $dp[0] = 1$

我们得到以下式子:

考虑如何理解这个式子。

我们枚举上一个不受限制的位置 $j$,然后乘上对应的容斥系数和计算转移系数,最后加上全部受限制的情况。

因为要优化,所以把式子小小的变一下:

传说中的分治 $NTT$ 可以解决这一类 $dp$ 的优化问题,它的核心思路大概是这样的:

分治 $NTT$ 解决形如这样的问题:

假设要求的函数为 $f$,有另一个函数 $g$。

满足 $f(i) = \sum\limits_{j\ < i} f(j) \times g(i-j)$。

类似于 $\text{CDQ}$ 一样,考虑左边对右边的贡献即可,容易发现这是一个好做的卷积形式。

对于这道题来说,$-1$ 的次幂可以被拆到两边,剩下的事情有手就行。

收获

容斥原理可以解决这样一类问题,有一个全集 $S$,$|S|$ 好求,现在有若干属性 $p_i$,构成集合 $T$,需要求满足属性集合 $T$ 的元素个数。并且可以很容易求出这样一种情况的答案:限定某些属性不满足,其它属性不做要求。

20220320

HaltOJ 7

思考:

想一下小学的时候做过的奥数题,一个圆里画 $n$ 条线最多分成几部分,答案是 $n*(n+1)/2 + 1$。再考虑下平行线和多点共线的情况,发现答案只和每个交点的情况和交点个数有关。手玩一下可以发现,在逐个加入直线的情况下,部分的个数增量为此条直线和其它所有直线交点个数 $x$,再加上 $1$,注意,相同交点只算一个。

证明可以参考平面欧拉定理,此处不做赘述。

所以我们模拟这个过程,每次加入一条直线判断新增了多少交点,具体可以先暴力枚举直线,求出所有交点之后带上 $eps$ 去重。然后发现 $y$ 只和 $x$ 有关,所以只用计算一个。然后我们发现如果按照斜率为第一关键字,截距为第二关键字排序加入线段,那么 $x$ 是一个相当优美的形式 $\dfrac{j-b}{a-i}$,$a,b$ 表示当前线段的斜率和截距,$i,j$ 表示枚举的线段的斜率和截距,$i,j$ 的取值都连续,所以这个式子中,分母会取遍 $[1,i]$,分子会取遍 $[-b,B-j-1]$,正负数分开考虑,现在问题变成了问 $\dfrac{[1,x]}{[1,y]}$ 中有多少个不同的数,$A^2$ 预处理,$O(1)$ 回答即可,约定每个数在最简分数被统计。

可以莫比乌斯反演优化,这个可以很方便的转化成 $[gcd(x,y)=1]$ 的形式并整除分块计算,可以 $n\sqrt n$,但没必要。

HaltOJ8

思考

这道题出出来就展现出对直接 $dp$ 的恶意,无论那种合并方法都无法解决这个问题。我们不妨另辟蹊径,考虑字符串的另一种生成方式——插入。

具体的,我们将不同的花视为不同字符,那么我们需要生成一个字符串,相邻字符不同,每个字符个数指定。

我们发现当前的插入方式仅仅受到当前相同字符位置个数的限制,所以设 $f_i$ 表示考虑到现在,有 $i$ 个字符相同位置的方案数。

转移可以枚举当前字符划分为多少段,其中有多少段插入相同字符位置,具体每一段放几个可以通过插板法计算方案。这样的转移看上去是 $O{(10^{5}})^3$,但是如果我们将字符按个数排序后并按照 $3,1,2,4$ 的顺序插入,因为保证了有两个只有 $200$,所以复杂度为 $O(1+200^2 + 200^3 + 10^5)$ 的复杂度,最后一次转移强制要求了段数和插入相同字符位置的数量,第三次则是因为第二次转移后有效位置仅有 $200$ 个。

听说可以做到 $O(200^2 + 10^5)$ ,但显然我不会。

20220322

ARC137D

题意

给定一个序列 $a$,反复做前缀异或操作,问若干次操作后 $a_n$ 的值,询问所有 $[1,k]$ 的答案。

思考

考场上一直在考虑分析单纯的 $01$ 系数,而忽略了系数之间的内在其它联系,事实上,对于反复执行的可加前缀操作,设距离为 $d$,操作次数为 $k$,那么贡献应该为从 $(0,0)$ 走到 $(d,k-1)$ 的方案数。证明比较简单,将原点的贡献转移拆分到横坐标即可。

更严谨的证明可以使用归纳法,记贡献函数为 $f(x,y)$,$f(n,k) = f(n-1,k)+f(n,k-1)$,那么考虑其组合意义,第一项代表了前 $n-1$ 项的和,第二项则是自己在上面的步骤中的累计。

那个 $-1$ 很讨厌,先不管。

用组合数的形式表示答案,即为 $\tbinom{n+k}{n}$,对其应用卢卡斯定理求出 $\bmod2$ 的结果,发现当且仅当 $n\&k = 0$ 时有值,那么对于一个固定的 $n$ ,有值的 $k$ 一定可以描述为一个 $s$ 在二进制意义下的子集。

然后对其做一次 $\text{FMT}$ 变换即可得到结果。

注意处理被忽略的 $-1$

20220323

CF1657D

思路

先做乘法转换,这个没啥说的。然后我考虑的是根号分治,先把不在凸包上的扔掉,对于代价大于 $B$ 的,枚举选的个数,然后尺取法搞定,对于代价小于 $B$ 的,直接计算。场上过了,赛后被叉。实际上,对于这种整除的题目,我们都可以考虑枚举倍数约数,然后可以直接计算代价为 $[1,C]$ 的最大权值,然后直接在上面二分就行。

CF1647E

思路

不难发现充要条件是每个点向 $1$ 的边为最小值。然后考虑 $dp$,直接对点 $dp$ 不太好做,我们发现它是有关大小的,所以考虑按照向 $1$ 的边权值从小到大 $dp$,每次枚举一段连续的区间,以及填的值转移,转移是一个前缀和乘上一个组合数再乘上一个幂次。

CF1647F

思路:

看上去就非常暴力,对每个限制建点,两种状态,限制和树上的点的状态推出关系连边,然后暴力确定每个限制的状态看是否冲突即可。实际上这就是模拟了 $\text{2-SAT}$。

考虑这个东西为什么和 $\text{2-SAT}$ 的正常做法一样的,正常做法是找强连通分量, 拓扑排序后对每个点选拓扑序大那个值。和我们的暴力模拟过程没啥区别。

HaltOJ126(2-SAT 的简单理解)

思考:

有一些比较奇怪的限制,然后每个人在每个点就两种状态,要想起一个东西叫 $\text{2-SAT}$,我们令 $statu[i][j]$ 表示 $i$ 子树内是否有 $j$,那么很容易构造出标准的 $\text{2-SAT}$ 限制,然后我们考虑约束,分类讨论一下。以 $\text{2-SAT}$ 的形式来说,对于 $x$ 的每个儿子 $v$,两个点都不能同时在 $v$ 中。同时,如果 $lca(x,r) \neq x$ ,那么两个人都必须在 $x$ 子树内。如果 $lca(x,r) = x$,那么 $a,b$ 只能有一个在子树外,就是一个为假那么另一个为真,除此以外,如果 $x \neq r$,那么 $a,b$ 不能在 $x$ 向 $r$ 的儿子里。

然后跑一个标准的 $\text{2-SAT}$。

$\text{Tarjan}$ 跑出来的 $\text{SCC}$ 编号是反拓扑序。

$\text{2-SAT}$ 的一些限制:强制 $u$ 为真,那么把假连向真,反之亦然。其它情况下注意逆否命题也要连边

$\text{2-SAT}$ 图的一些性质:$u \rightarrow v \Rightarrow \overline{v} \rightarrow \overline{u}$

$\text{2-SAT}$ 合法性:同一变量的两个状态不在同一 $\text{SCC}$ 内是存在方案的充要条件,必要性显然,充分性用构造法证明。

$\text{2-SAT}$ 方案构造:依次考虑每个变量,选择 $\text{SCC}$ 编号较小那个值,那么同一变量不会直接冲突,假设先前的点 $v$ 推出了 $\overline{u}$,并且 $\overline{u} \rightarrow u$,因为刚刚提到的性质,一定有 $u\rightarrow \overline{v}$,我们一定会选拓扑序较大的 $\overline{v}$,因此选择不会冲突。

20220325

CF1656D

题意:

  • 您有一个 $n$,您需要找一个 $k \in[2,\infty)$,使得 $n$ 可以被表示为 $k$ 个模 $k$ 意义下不同的数,多解可以输出任意一个。

  • 多测 $T\leq 10^5,n\leq10^{18}$

思考:

因为要找的 $k$ 个数模 $k$ 意义下不同,我们又知道任意一个数 $a$ 都可以表示为 $a=b \times k +r$,所以不妨先把模 $k$ 的余数和,也就是上式中的 $r$ 提取出来,我们得到了一个新的式子。下面设 $n$ 为 $k$ 个数的和。

其中 $c$ 表示这 $k$ 个数对应 $b$ 的和。分母让人很不爽,所以乘过去。至于为什么是 $(k+1)\times k$,是为了让 $c$ 可以取到 $[0,\infty)$

$2n$ 的两个因子奇偶性不同,所以如果我们的 $n$ 为奇数就可以直接令 $k=2$,否则我们每次令 $k$ 取 $2,4,8,\cdots$,直到 $\dfrac{2n}{k}$ 为奇数为止,这个时候记 $res = \dfrac{2n}{k}$,如果 $res$ 较大,则取对应的 $k$,否则取 $k=res$。

注意特判掉一些边界情况,比如 $2$ 的次幂,$2$ 的次幂,还有 $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
#include<bits/stdc++.h>
#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 _type>void cmin(_type &a,_type b){a=min(a,b);}
template<typename _type>void cmax(_type &a,_type b){a=max(a,b);}
int i,j,k,n,s,t,m;
signed main()
{
read(t);
while(t--){
read(n);int ans=-1;
for(i=2;i<1ll<<62;i*=2){
int res=2*n/i;
if(res%2){
if(res>i){
ans=i;
break;
}
else{
ans=res;
break;
}
}
}
if(ans==1)puts("-1");
else printf("%lld\n",ans);
}
return 0;
}

CF1656E

题意

  • 您有一棵 $n$ 个点的无向无根树,您需要为每个点安排一个权值 $a_i\in[-10^5,10^5]$,使得删掉任意一个点之后剩下连通块的权值和相同,注意安排的权值不能为 $0$。
  • 多测 $T\leq 10^4,\sum n \leq 10^5$

思考

诈骗题。我们考虑指定一个根,就让它是 $1$,然后随便删一个点 $u$,那么 $u$ 的所有儿子的子树都必须有同一个值,我们可以尝试安排每一棵子树的权值和,这是可以做到的,因为可以让根控制这个值。不妨安排每颗子树的权值和为 $1$, 那么我们再安排整个树的权值和为 $2$,就可以让删掉每个点 $u$ 后剩下的连通块权值和相同。$u$ 的所有儿子子树的权值和都为 $1$,而除开 $u$ 的子树后的那个连通块的权值和就是整棵树的权值和 $2$,减去 $u$ 子树的权值和,就是 $1$,也和 $u$ 所有儿子的子树的权值和相同。

但是有一个问题,如果一个点 $u$ 只有一个儿子,那么 $u$ 的权值会被安排为 $0$,是不合法的,所以我们需要更改一下安排的方式,对于一个点,设它子树的权值和为 $x$, 并且它所有儿子的子树的权值和均为 $y$,而且整棵树的权值和为 $x+y$。这是合法的必要条件,因为我们需要让 $a_i \neq0$,所以安排根的权值为 $0$,其它点按深度模 $2$,的值安排 $-1$ 和 $1$ 即可。

代码

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
#include<bits/stdc++.h>
#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 _type>void cmin(_type &a,_type b){a=min(a,b);}
template<typename _type>void cmax(_type &a,_type b){a=max(a,b);}
const int N=1e6+10;
int i,j,k,n,s,t,m;
vector<int> e[N];
int val[N],fa[N],dep[N],deg[N],tar[N];
void dfs(int u){
if(u==s)tar[u]=val[u]=0;
else tar[u]=val[u]=dep[u]%2?1:-1;
for(int v:e[u]){
if(fa[u]==v)continue;
dep[v]=dep[u]+1,fa[v]=u;dfs(v);val[u]-=tar[v];
}
}
signed main()
{
read(t);
while(t--){
read(n);dep[1]=1;s=1;
for(i=1;i<=n;i++)e[i].clear(),val[i]=0,tar[i]=deg[i]=0,fa[i]=0;
for(i=1;i<n;i++){
int x,y;read(x),read(y);
e[x].push_back(y),e[y].push_back(x);
deg[x]++,deg[y]++;
}
for(i=1;i<=n;i++)if(deg[i]>deg[s])s=i;
dfs(s);
for(i=1;i<=n;i++)printf("%d ",val[i]);
puts("");

}
return 0;
}


CF1656F

题意

  • 您有 $n$ 个点,每个点有一个权值 $a_i$,定义一张有权无向完全图 $K(t)$ 为每个点 $i$ 向 $j$ 连一条权值为 $a_i\times a_j+(a_i+a_j)\times t$ 的无向边后所构成的图,定义 $f(t)$ 为 $K(t)$ 最小生成树的权值和。您需要对所有的实数 $t$ 求出 $f(t)$ 的最大值并输出它,如果最大值不收敛,那么输出 INF

  • 多测,$T\leq 10^4,\sum n\leq 2\times 10^5,-10^6\leq a_i\leq 10^6$

思考

场上差那么一点点就 15Ton 了

一个瓶颈在排序的线性做法。

记 $d_i$ 表示点 $i$ 的度数。

判断 $\text{INF}$ 是简单的,只需要看能不能凑出 $\sum a_i\times d_i$ 分别为正和负或者 $0$ 就行,如果凑不出来,令 $t$ 为正无穷或者负无穷,然后它就不收敛了。

现在已知有解。

考虑固定一个 $t$ 后怎么快速求 $\text{MST}$。做个恒等变形,权值为 $(a_i+t)\times (a_j+t)-t^2$,把 $t^2$ 给扔掉。

结论是排序后按正负性断开,$a_1$ 向所有 $a_i$ 大于 $0$ 的连边, $a_n$ 向所有 $a_i$ 小于 $0$ 的连边,$0$ 无所谓。

证明可以考虑 Prime 算法的过程,最开始一定是 $a_1$ 到 $a_n$。然后后面不会取到正负性相同的,并且一个点一定是连向 $a_1$ 或者 $a_n$。

如果处理一个前缀和,知道正负交界的位置之后可以快速算,从小到大枚举 $t$,然后双指针维护交界处。

可以证明 $t$ 一定取到每个 $-a_i$。如果夹在两坨中间,那么由于具体选哪些边是固定的,根据 $\sum a_i\times d_i$ 正负性调整即可。

不会取到 $[a_1,a_n]$ 外面去,因为我们已经判了无解,所以取到边界外面时 $\sum a_i\times d_i$ 的正负性会导致向里面调整更优。

20220329

HaltOJ129(Powerful Number 筛)

思考:

打个表发现 $f$ 是积性函数。

然后 $f(p^c)$ 是好求的,考虑亚线性筛法。

我也不知道为啥会想到 PN 筛,总之这种东西各种筛法都可以尝试一下。

PN 筛和其它亚线性筛法一样,是用来求一些积性函数的前缀和的,它的关键在于构造一个好求的前缀和的 $g$,满足 $g(p)=f(p)$,然后构造一个 $h$ ,满足 $f=h * g$,乘法为迪利克雷卷积。

这里我们构造 $g(x)=1$

积性函数有个性质,$f(1)=1$,所以展开下 $f$,发现 $f(p)=1=h(1) g(p)+h(p) g(1)$,然后 $h(p)=0$,由于 $h$ 也是个积性函数(迪利克雷卷积的性质),所以 $h$ 只会在 PN 处有取值,PN 的定义为每个质因子次数都大于等于 $2$ 的数。

可以证明所有比 $n$ 小的 PN 个数是 $O(\sqrt n)$ 的 。

可以预处理 $\sqrt n$ 以内的所有质数,然后枚举指数得到每个 PN,这个 $dfs$ 的过程中可以记录一下对应的 $h$,$h$ 的转移是好做的,因为只需要求 $h(p^c)$。

这里不难发现 $h(p^c)=f(p^c)-f(p^{c-1})$。

原因是 $g$ 实际上是 $1$,而 $f / 1 = f * \mu$

20220331

20220330考试T2

思考

限制不好弄,考虑转化限制,不难发现,如果建图,并依次加入有向边,那么任意一个时刻,都需要满足当前图及其补图都是一个传递闭包,即,如果能间接 $a\rightarrow b$ ,那么 $a,b$ 有边。这样的图不是很多,是 $n!$ 个的,只需要考虑图之间的转移就行。

这样的图和一个 $n$ 的排列一一对应,构造方案为如果 $a,b$ 为逆序对,那么加入一条边 $(a,b)$。加入一条边时,只需要枚举相邻的逆序对并交换,转移可以康托展开得到交换后的编号。限制也比较好做,转移的时候看看强制在前面的边在不在里面就行。

复杂度为 $O(n!\times n \times (n+m))$,常数较小,可以过。

20220402

20220401考试T1

思考

对于这种翻转的博弈问题,其实都可以做一个转化:不要翻转颜色,而是在每个翻转的地方再放一个棋子,这样不会改变游戏的胜负性,因为如果原处没有棋子,那么等效,如果有棋子,那么有两个,在 $SG$ 的意义下,这是可以抵消的。如果以组合方式理解,那么因为放了之后如果有人操作了,那么另一个人把它的操作复制一遍即可,由于两个人都是最优操作,所以可以抵消。

有了这个转化,每个棋子都变成了一个独立的游戏,该局面的 $SG$ 值就不难计算了,按照定义计算出每个位置的 $SG$ 值然后异或查表即可。

20220401考试T3

思考

场上写了个 $O(n^3)$ 卡常卡过去了,实际上 $O(n^2)$ 的有点难想,它是把构造表达式的过程视作了一个添加左右括号的过程,总之非常神仙。

有个对于区间 $dp$ 的常数优化思路,可以改变一下枚举顺序,让数组访问尽量连续,以便最大程度利用好 $L1$,可以节省大量内存操作时间。

咕一篇常数优化文章。

2022.2.7

CF1634E

题意:

给定一些数组,长度都为偶数,需要将每个数组的元素划分到两个可重集合里,要求两个集合相同,问方案或输出无解,$n,m=10^5$。

思考:

考虑每个数组单独划分,不太行,所以不能一个数组一个数组的考虑,显然有解的必要条件是每个数字个数为偶数。所以一个数字一个数字的考虑,其实也不太行,因为这和刚刚那种方法是本质上一样的。

注意到也保证每个数组长度为偶数(这不是废话吗),都是偶数,不由得让我们想到欧拉回路,所以尝试建图,每个不同的数和数组视为一个点,由数组向数连边,数组内存在一个数,就由数组向这个数连一条边,现在需要给图定向为一张欧拉图,我们将数组向数连的边是为将这个数分到第一个集合,反之则为第二个,构造欧拉图即可。

实现上的问题:

存边用 set 存,不然 TLE 没商量。

图不一定联通,所以一定要多起点。

20220209

22020208考试 T1

题意:

定义矩阵 $Mat_n$

  • $n = 0$ 时,为一个 $1$
  • $n > 0$ 时,左上角为 $0$ 矩阵,其余部分为三个 $Mat_{n-1}$。

在一个平面上画出两个 $Mat_n$,左下角分别为 $(0,0),(x,y)$,求都为 $1$ 的位置有多少个。

思考:

考虑旋转矩阵转化为位运算计数问题,转化为求整数对 $(i,j)$ 满足 (i&j) == 0 and ((i+x)&(j-y)) == 0

考场上看到要高精就润了,没有认真思考。

实际上可以考虑类似 数位dp 的算法,也不难。

22020208考试 T2(后缀自动机复习)

题意:

给定一个字符串 $s$ ,$q$ 次询问 $l,r$,表示所有 $[l,r]$ 开头的子串中本质不同的子串个数。

$n,q = 2 \times 10^5$

思考1:

考虑后缀自动机求本质不同子串个数的方式,发现它可以在线完成。

所以反着建后缀自动机。

对于 $r=n$ 的部分分,显然先离线,可以建后缀自动机时统计下每一个结尾的答案,输出即可。

如果 $r \neq n$,那么不妨仍然考虑在后缀自动机上如何统计答案。

后缀自动机本质上维护的是 $endpos$ 集合,对于一个询问,如果一个 $endpos$ 集合中包含一个位置 $x\in[l,r]$,那么这个 $endpos$ 集合所代表的子串应该被计入贡献。

后缀自动机上每个子串仅在一个 $endpos$ 集合中出现,所以可以求出至少有一个元素被包含的集合,并直接加和。

考虑对于每个询问该如何统计答案,限制一共有两个 $l,r$,并不好做,然而我们发现后缀自动机是在线构建的,所以如果将询问离线,那么限制就只剩下一个 $r$ 了。我们在构建后缀自动机的同时只需要记录每个 $endpos$ 集合最小的元素 $val$,查询时查询所有满足 $val \leq r$ 的集合的子串数量和即可。考虑维护一个数组 $c$,$c[i]$ 表示 $val = i$ 的 $endpos$ 集合子串个数和,用 $BIT$ 查前缀和,现在只剩下修改了。

考虑 $endpos$ 树的性质,发现修改操作是把一段到根的路径赋值,同时还有修改父亲等操作,于是考虑动态树,我们不难发现一段实链上的集合,$val$ 都是相同的,所以在每个节点维护一下 $val,sum$ 值即可,注意 $push_down$ 操作时需要把赋值也 $push_down$ 下去。

这样做未免显得有些麻烦,我们考虑最终构建的后缀自动机,倒序激活 $endpos$ 中每一个点,这样就不需要 $Link$ 和 $Cut$ 操作,只需要写 $access$ 就行。

实际上未必需要用后缀树来实现,通过激活点的方式,树已经时静态的了,这本质上还是一个区间染色问题,我们完全可以直接重链剖分,在每条重链上开个栈维护断点,记录下前缀和。

复杂度都是 $O(nlog_n^2)$,个人认为类似动态树的方法会好写一些。

思考2:

考虑使用后缀数组,回想后缀数组统计不同子串个数的方式,实际上就是排序后减掉相邻两个的 $lcp$,于是对原串后缀排序,然后离线询问莫队,拿个 set 维护当前所有串的排名集合,再来个 $ST$ 表计算 $lcp$,插入和删除都很好写,复杂度 $O(n\sqrt n \ log_n)$,难以通过本题。

考虑优化,有一个技巧,这种需要查前驱后继的东西,实际上可以用链表搞,加入相对困难一些,因为我们不知道到底应该放在哪里,但删除就很容易了,双向链表上直接删就完事了,所以可以回滚莫队,非常无脑,时间复杂度 $O(n\sqrt n)$。

稍加卡常即可通过,$ST$ 表查询常数相对较大,卡常应考虑尽量减少查询次数。

20220210

20220208考试T3

恶心的题意

思考

发现由于编号连续,所以最后经过的一定是一段连续区间。想到可以枚举右端点,二分左端点。

一个区域到另一个区域的路径可以分为区域到中转站和中转站到中转站两部分,区域到中转站的情况可以只计算到左右两边最近的中转站,所以容易计算。

我们按照右端点顺序激活中转站,问题就是要最小化左端点。

两个中转站只能通过同一条线连接,把最低点也看成中转站,所以可以认为从一个中转站到另一个的代价为两者所在区域编号的较小值。

中转站到中转站的距离可以 Floyd 暴力,我们就已经计算出了每对中转站点相互抵达的代价,所以二分都不需要了,我们可以直接用这个代价计算出最终答案。

实现细节

Floyd 的时候,因为加入的中转站并不是按下标顺序,所以需要整个跑一遍。

20220211

20220211 测试 T1

题意

思考

这道题算是考场上想出来的,考场上摸了一会儿鱼,发现有个地方假了的时候只剩 $5min$ 了,所以紧急修复成了$50pts$,实际上不管直接交有 $80pts$。

先把最上面那些没有的行删掉。

考虑每一行的可能性,只有穿过和不穿过两种,穿过又可以分为去的时候穿过和回的时候穿过。

来的时候穿过,回的时候一定不会穿过,因为一定不优,于是三进制枚举穿过状态,大的路径框架已经被构建出来了,现在要考虑经过那些没有穿过的点。

被穿过的行不用管,现在就剩没穿过的行,每一行可以从左到右,也可以从右到左,如果两边都有经过,还可以两面包夹,算出每一行的结果,然后加上去就行。

这样可以拿到 $80pts$,因为它处理不了这种情况。

1
2
3
4
3 7
#.....#
.#####.
.#####.

它会穿过第一行和第二行,到第三行后穿过或者是走到底再回来,实际上在穿过第一行走到第二行的时候就应该上去拿掉右上角。

所以把大的路径画出来后,记录每一个边缘位置是否到达过,还要自下而上 $dp$ 一遍求出经过所有未经过的点的最小代价。

提供一种思路,设 $dp[i][j][k]$ 表示到第 $i$ 行,左右最前面的可以已经经过的点为 $j,k$ 的最小代价,转移的时候看 $j,k$ 是否大于 $i$ 或者 $i$ 的两端是否已经经过,如果可行,就选经过一行三种方案的最小值,如果不行,因为一定有一边已经经过,所以只考虑另一边是否补全和从经过的点走一遍再回来的情况,补全的方案有两种,从上面和从下面,直接转移即可。

时间复杂度 $O(n^3 * 3^n)$,实际上剪枝后跑的飞快,$10ms$ 就跑完了。

20220212

20220212测试T2

题意

思考:

考虑直接 $dp$ 转移,设 $dp[i][j][k][0/1]$ 表示考虑前 $i$ 个数, 正色子已经用了 $j$ 个,反色子已经用了 $k$ 个,是否已经赢了的方案数,直接转移是 $O(n^4 * t)$ 的,可以拿到 $20pts$ 的好成绩。

考虑优化,转移式子是:

1
2
3
4
5
6
7
8
9
10
11
12
for(int i1=0;i1<=n;i1++)
for(int i2=0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
for(int j2=0;j2<=i2;j2++)
{
if(i<=t&&i1-j1>i2-j2)
Inc(dp[i][i1][i2][1],1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);
else
Inc(dp[i][i1][i2][0],1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);

Inc(dp[i][i1][i2][1],1ll*dp[i-1][j1][j2][1]*C[n-j1][i1-j1]%mod*C[m-j2][i2-j2]%mod);
}

转移目标

dp[i][i1][i2][1]

前面一部分

1ll*dp[i-1][j1][j2][0]*C[n-j1][i1-j1]

和后面一部分

C[m-j2][i2-j2]

只有前一部分依赖 $j1$,考虑能不能预处理前一部分的转移,然后优化一个 $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
for(int j1=0;j1<=n;j1++)
for(int i2=0;i2<=m;i2++)
{
sum[j1][i2]=1ll*dp[i-1][j1][0][1]*C[m][i2]%mod;
sum2[j1][i2][0]=1ll*dp[i-1][j1][0][0]*C[m][i2]%mod;
for(int j2=1;j2<=i2;j2++)
{
if(dp[i-1][j1][j2][1])
Inc(sum[j1][i2],1ll*dp[i-1][j1][j2][1]*C[m-j2][i2-j2]%mod);
sum2[j1][i2][j2]=sum2[j1][i2][j2-1];
if(dp[i-1][j1][j2][0])
Inc(sum2[j1][i2][j2],1ll*dp[i-1][j1][j2][0]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=0;i1<=n;i1++)
for(int i2=0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
{
int val=C[n-j1][i1-j1];
Inc(dp[i][i1][i2][1],1ll*val*sum[j1][i2]%mod);
if(i<=t)
{
int pos=i2+j1-i1,val1=pos<0?0:sum2[j1][i2][pos],val2;
val2=sum2[j1][i2][i2];Dec(val2,val1);
if(i<t)Inc(dp[i][i1][i2][0],1ll*val*val1%mod);
Inc(dp[i][i1][i2][1],1ll*val*val2%mod);
}
}

优化到了 $O(n^3*t)$,记此时的常数 $k = 1$,它需要约 $15s$ 通过数据规模最大的测试点。

这份代码可以拿到 $50pts$ 的好成绩,感觉上已经不太能优化复杂度了,我们考虑卡常。

第一步,发现 $n=6$ 的时候只需要转移 $dp[6][n][m][1]$

第二步,发现 $n=1$ 的时候被转移的只有 $dp[0][0][0][0]$

常数变为 $k = \dfrac{2}{3}$

此时可以拿到 $70pts$ 的好成绩。

继续考虑卡常。

发现其实在 $i>=t$ 时转移 $dp[i][i1][i2][0]$ 没有意义,直接剪掉。常数没有变化。

发现 $dp[i][i1][i2][1]$ 实际上可以由 $dp[i][i1][i2][0]$ 直接得到,所以只转移 $dp[i][i1][i2][0]$ ,记作 $dp[i][i1][i2]$,常数变为 $k = \dfrac{1}{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
if(i<=t)
{
for(int j1=0;j1<=n;j1++)
for(int i2=i==6?m:0;i2<=m;i2++)
{
sum[j1][i2][0]=1ll*dp[i-1][j1][0]*C[m][i2]%mod;
int *p=&sum[j1][i2][1];
for(int j2=1;j2<=i2;j2++)
{
*p=sum[j1][i2][j2-1];
Inc(*p++,1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=i==6?n:0;i1<=n;i1++)
for(int j1=0;j1<=i1;j1++)
for(int i2=i==6?m:max(0,i1-j1);i2<=m;i2++)
Inc(dp[i][i1][i2],1ll*C[n-j1][i1-j1]*sum[j1][i2][i2+j1-i1]%mod);
}
else
{
for(int i2=i==6?m:0;i2<=m;i2++)
{
for(int j1=0;j1<=n;j1++)
sum[j1][i2][0]=1ll*dp[i-1][j1][0]*C[m][i2]%mod;
for(int j2=1;j2<=i2;j2++)
for(int j1=0;j1<=n;j1++)
Inc(sum[j1][i2][0],1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
for(int i1=i==6?n:0;i1<=n;i1++)
for(int i2=i==6?m:0;i2<=m;i2++)
for(int j1=0;j1<=i1;j1++)
Inc(dp[i][i1][i2],1ll*sum[j1][i2][0]*C[n-j1][i1-j1]%mod);
}

此时的成绩还是 $70pts$,但我们可以观察一下代码,在 $i>t$ 后的转移实际上没有任何意义,可以直接计算,优化这一部分,可以拿到 $90pts$ 的好成绩。

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
for(i=2;i<=min(t,5);i++)
{
for(int j1=0;j1<=n;j1++)
for(int i2=0;i2<=m;i2++)
{
sum[j1][i2][0]=1ll*dp[i-1][j1][0]%mod*C[m][i2]%mod;
int *p=&sum[j1][i2][1];
for(int j2=1;j2<=i2;j2++)
{
if(dp[i-1][j1][j2]>=mod)dp[i-1][j1][j2]%=mod;
*p=sum[j1][i2][j2-1];
Inc(*p++,1ll*dp[i-1][j1][j2]*C[m-j2][i2-j2]%mod);
}
}
for(int i1=0;i1<=n;i1++)
for(int j1=0;j1<=i1;j1++)
for(int i2=max(0,i1-j1);i2<=m;i2++)
{
dp[i][i1][i2]+=1ll*C[n-j1][i1-j1]*sum[j1][i2][i2+j1-i1];
if(dp[i][i1][i2]>=8e18)dp[i][i1][i2]%=mod;
}
}
if(t==6)
{
for(i=0;i<=n;i++)
for(j=0;j<=m;j++)
Inc(dp[6][n][m],(n-i<=m-j)*dp[5][i][j]%mod);

printf("%lld\n",((quick(6,n+m)-dp[6][n][m]%mod)+mod)%mod);
}
else
{
int all=0;
for(i=0;i<=n;i++)
for(j=0;j<=m;j++)
Inc(all,1ll*dp[t][i][j]%mod*quick(6-t,n+m-i-j)%mod);

printf("%d\n",((quick(6,n+m)-all)+mod)%mod);
}

最后的 $10pts$ 可能需要一些卡常技巧。

我们发现实际上复杂度的瓶颈在数组寻址和取模,考虑优化其中之一,被寻址的数组经过循环变量的调整已经相当连续了,我们考虑优化取模。

long long 存 $dp$,每 $8$ 次加法取一次模,可以通过本题。

一个很常见的,在取模运算较多时的优化技巧。

20220211 测试T3

题意

思考:

这道题暴力思路无非就二进制枚举和容斥。正解的做法很神仙。

在这种 $\text{NP}$ 问题中,我们完全可以考虑将枚举的部分减少,这道题的思路是对稀疏图删点变为森林,然后二进制枚举一些删掉的点的状况再进行树形 $dp$,着实是一个很有启发性的思路。

20220212

CF1637F

思考:

对于这种条件为最小值的覆盖问题,我们不妨考虑最大的那个点是如何被覆盖的,首先有一个结论是只会在叶子有基站,证明简单,略去。然后考虑最大的那个点是如何被覆盖的,把它看作根,一定来源于它不同儿子的两个叶子,所以我们考虑其它点的时候一定可以认为这个根上的值为 $\text{INF}$ ,理由不多说了。所以每个点 $u$ 的包含的叶子至少有一个最大值为 $h_u$,贪心即可,最后决策最大值的点应该放在哪里,同样的贪心。

实现较为简单,没什么说的。

20220225

haltoj116

思考:

考虑团和独立集的性质,我在考场上就想到了答案一定不多,极大概率无需取模这一特性,于是用类似分治的方式得到了 $60pts$,如果我们找出了一个合法解,那么其它合法解的构造也是简单的,考虑合法解的性质,不妨设团的大小为 $s$ ,那么因为独立集只能向团连边,所以独立集中的点最大度数为 $s$,而这样又导致了团中的点最小度数增加,所以我们不难发现团中的点一定是度数最大的那一段前缀。

对于度数相同的点,看似无法下手,但是我们知道边的构成是团内加上团和独立集之间,团内的边数我们是知道的,而如果记录一下团中点的总度数,我们可以很轻松的推出独立集间是否有边,我们设团内点的总度数为 $p$,团的大小为 $i$,团内边数为 ,那么其他点之间的边数就为 $(2m-2p+i(i-1))/2$,所以我们考虑满足 $2m + i (i-1) = 2p$ 这个条件的选择有什么性质,因为这是成立的必要条件,考虑证明这也是充分条件。如果团内边数不足 $i*(i-1)$,那么我们发现 $p$ 这边会减小。考虑如果独立集内边数有边,那么 $p$ 这边一样相对减少,所以这也是充分条件。

这道题结束了。

haltoj117:

思考:

考虑在原序列中是前缀 $max$ 的点,它们在新划分的序列中一定也是前缀 $max$,如果新增了前缀 $max$,那么我们可以很轻松的改变划分情况去除这个前缀 $max$,所以如果原序列中前缀 $max$ 的数量为偶数,就一定可以。如果为奇数,我们不妨考虑只有一个序列有新增的前缀 $max$ 的情况。我们考虑从原序列中抽出一个新序列出来,设两个序列的权值之差为 $k$,如果抽了一个原先的前缀 $max$,那么 $k$ 减少 $2$,如果新增了一个前缀 $max$,$k$ 减少量为 $1$,所以我们将原先是前缀 $max$ 的数的权值视为 $2$,其余的视为 $1$,问题就是是要选一个上升子序列,使得权值和为前缀 $max$ 的个数,这个问题就相当简单了。

20220228

ABC215H(子集容斥的另一种思路)

考虑霍尔定律,枚举不满足条件的子集 $mask$,可以用 $\text{FMT-DP}$ 计算出必须由 $mask$ 供给的白菜数量,考虑减少一个子集中的白菜使得不满足条件,就可以计算出最少需要吃多少个。问题变成了统计答案,我们发现对于一个 $mask$ 直接组合数计算然后相加会算重,又因为答案不是全集所以并不能简单容斥。而暴力计算强制每种白菜都选一个的复杂度是 $O(3^n)$ 的,我们仍然考虑应用 $\text{FMT-DP}$ 计算答案。事实上,这种组合数问题都可以考虑这种方式。

设 $f_{mask}$ 表示所选白菜集合被 $mask$ 包含的方案数,设 $f’_{mask}$ 表示所选白菜种类集合恰好为 $mask$ 的方案数,列有等式。

令 $f[i][mask]$ 表示当前子集为 $mask$ ,低 $i$ 位不一定存在,但高位都严格符合要求的方案总数,列有

1
2
3
4
5
for(i=n-1;i>=0;i--)
for(int mask=0;mask<1<<n;mask++){
f[i][mask]=f[i+1][mask];
if(1<<i&mask)f[i][mask]-=f[i+1][mask^(1<<i)],f[i][mask]%=mod;
}

从 $i+1$ 推到 $i$ 的过程中,因为高位已经强制存在了,所以我们减去的就是 $f[i][mask]$ 中所有不存在第 $i$ 位,但高位符合要求的情况。

最后计算出来的 $f[0]$ 就是上述的 $f$。

然后这道题就好做了。

20220301

考试 T1(SAM的进一步理解)

题意:

给定一颗字符在点上的 $\text{Trie}$,求 $\text{Trie}$ 代表的所有字符串本质不同子串个数,顺便询问钦定字符大小关系后第 $k$ 小的子串,保证 $\text{Trie}$ 上字符随机且答案长度和不超过 $800KB$。

思考:

考场上想到可以类似 $\text{SAM}$ 一样乱搞,然后就写了 $\text{dfs}$ 构建 $\text{SAM}$,$lst$ 指针被置为它父亲插入后的 $p$,这个做法在数据随机的情况下是没有问题的,但是如果出现构造数据,它会没掉。

首先是如果拉一条 $a$ 链,链上每个点再挂一个 $b$,然后 $dfs$ 回跳的时候重置 $q$ 的来边到 $np$ 时时间复杂度是 $O(n^2)$ 的。

我所理解的 $\text{SAM}$ 时间复杂度正确性基于两个东西,一个是 $p$ 的深度变化情况,这证明了第一次构建边的时候时间正确。在 $dfs$ 构建 $SAM$ 的时候,大量回溯操作会导致 $p$ 深度的异常变化,这样就不对了。第二个是重置 $q$ 的来边的循环,这个可以分析 $p$ 的 $fa$ 所代表最短字符串长度来理解,每次重置 $q$ 的来边,我们都认为是找到了一个更短的 $np$ 所代表的字符串,不妨看成一个指针在插入的字符串上移动,它只会向右。重置完 $p$ 的来边再插入下个点的过程中,我们至少会跳到 $np$ 处(只针对单个串),然后重设的 $p’$ 的 $fa$ 的最短长度一定可以从 $np$ 那里继承过来,所以指针只会右移。$dfs$ 加入时同样导致了这个指针在 $\text{Trie}$ 上乱跳,复杂度就没有了保证。

然后是正确性相关的问题,这种 $\text{SAM}$ 写法会导致无效的空节点建立,比如说插入的时候就碰到了满足 $a[lst].ch[c]$ 存在的情况,这样新建出来的点实际上是无效的,在绝大部分题目中这个无效点是不影响答案的,但是少部分写法会导致爆炸。

接下来讨论下 $\text{BFS}$ 建树的正确性。由于是按深度加点,所以 $a[lst].ch[c]$ 一定是不存在的,因此绝不会导致无效节点的建立,时间复杂度证明不会,省略。

时间复杂度我并不会证明 emmmm.

ABC241H

思考:

生成函数套路题。

20220302

ARC136E

思考:

显然偶数链是很好走的,所以考虑从偶数边怎么到另一个节点,显然偶数只能选一个,我们先考虑不选偶数的情况。

定义 $f[x]$ 为 $x$ 的最小质因数,$x$ 能走到 $y$ 的充要条件是 $y>x,x+f[x]\leq y-f[y]$,如果 $(x,y) = 1$,那么显然,因为 $x$ 第一步至少走 $f[x]$,到 $y$ 的最后一步至少走 $f[y]$。如果 $(x,y) \neq 1$,设 $x = p \times f[x]$,则 $y$ 至少为 $(p+2)\times f[x]$,故结论仍然成立。

由此可以 $x$ 走不到 $y(y>x)$ 的充要条件为 $x+f[x]>y-f[y]$,将每个点视为 $(x-f[x],x+f[x]])$ ,题目就是要求出一些区间的集合,使得所有区间有公共点,要求权值最大,不妨枚举公共点,然后差分计算即可。

考虑下偶数,实际上做法类似。

20220303

AGC027E&&haltoj119

思考:

观察不变量是这种变换题的思考方向之一,我们设 $a$ 的权值为 $1$ ,$b$ 的权值为 $2$,总能发现权值和对 $3$ 取模的结果不变。

先考虑一个字符串在什么情况下可以变成另一个字符串,权值相同显然是必要条件,其次,源串 $s$ 不能是交替串 $abababa$ 这类,这有点困难,不妨先考虑变成一个字母的情况。

充要条件为权值相同且不交替,证明考虑归纳法。

现在考虑目标为一个字符串的情况。我们先贪心的匹配,用最少的字母构造出目标串,然后考虑调整剩下的部分使得它满足条件。如果特判掉交替串,我们发现只要权值相同且源串的一个前缀能和目标串贪心匹配上,那么一定可以变成目标串,于是考虑 $dp[i]$ 表示考虑到前 $i$ 的源串字母,能贪心的对应上多少不同串,显然这样的贪心对应是不重不漏的,转移分为匹配 $i+1$ 和不匹配 $i+1$ 两种,预处理一个类似 $nxt$ 的东西可以做到线性。

20220304

CF917D&&haltoj122(初探二项式反演)

思考:

考场上已经观察出原题需要求一张完全图有多少棵最小生成树与给定树至少有 $n-k-1$ 条边相同。$prufer$ 序列有一个结论,$n$ 个点 $k$ 个连通块的图构成有标号无根树的方案总数为

显然我不会证明。场上看到 $n$ 仅为 $50$ 就想乱搞,误打误撞弄了一个容斥 $dp$ 出来,实际上正解差不多就是这个。我们考虑钦定选了 $k$ 条边一定存在的方案总数 $f(k)$,由二项式反演的套路可以得知恰好 $k$ 条边存在的方案数 $g(k)$ 满足

其中 $C(i,j)$ 表示组合数。

显然我仍然不会证明,但是可以感性理解下,首先 $f(k)$ 满足以下式子

这个很好理解,枚举实际上选了多少条边,钦定 $k$ 条边的方案数就是从实际选的边中钦定一些出来。注意这个钦定和至少有区别,不是简单的后缀和,因为实际选一条边的方案可以被多种钦定的情况包含。

我不会证明二项式反演的式子,所以我们从容斥的角度来考虑 $g(k)$,第一项为钦定 $k$ 条边,然后减去被多考虑了的存在 $k+1$ 条边的情况,依此类推。组合数的存在则是因为 $f(i)$ 会被额外考虑 $C(i,k)$ 次。

$f$ 肯定比 $g$ 好算,我们现在需要计算的是选 $k$ 条边,然后得到的联通块组成不同有标号无根生成树的方案数。注意到生成树计数公式中 $n^{k-2}$ 与怎么选边完全无关,所以只需要记录一下 $\prod sz_i$,这个就可以 $dp[i][j][k]$ 表示考虑以 $i$ 为根的子树,选了 $j$ 条边,$i$ 所在连通块大小为 $k$ 的方案数。优化的话,可以考虑 $\prod sz_i$ 的组合意义。于是就是需要在每个连通块内选一个代表元,于是状态第三维简化为是否选了代表元,复杂度 $O(n^2)$。

写法优美的常用函数

收录一些优美的常用函数写法。

分解质因数

有人根本不会用 do whilevector

1
2
3
4
5
vector<int> v(0);
for(int i=2;i*i<=n;i++)if(n%i==0){
v.push_back(i);
do n/=i; while(n%i==0);
}

SCC

缩点。

1
2
3
4
5
6
7
8
9
10
11
12
int dfs(int u){
int low=dfn[u]=++df;st[++top]=u;
for(auto v:e[u]){
if(!dfn[v])cmin(low,dfs(v));
else if(!scc[v])cmin(low,dfn[v]);
}
if(low==dfn[u]){
++cnt;int v;
do val[cnt]+=a[v=st[top--]],scc[v]=cnt; while(v!=u);
}
return low;
}

最短路 Dijskstra

如果 $dis\times n\le 8\times 10^{18}$,完全可以它们压成一个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
priority_queue<LL,vector<LL>,greater<LL>> q;
void dij(int s,int t){
memset(vis,0,sizeof(vis));
memset(dis,1,sizeof(dis));
while(!q.empty())q.pop();
dis[s]=0;q.push(s);
while(!q.empty()){
int u=q.top()%M;q.pop();
if(vis[u])continue;vis[u]=1;
if(vis[t])return ;
for(int i=head[u];~i;i=a[i].next){
int v=a[i].v,w=a[i].w;
if(dis[u]+w>=dis[v])continue;
dis[v]=dis[u]+w;q.push(dis[v]*M+v);
}
}
}

网络流

内容主要为自己对网络流的一些理解和一些典例。

一些约定:

  • s 为源点,t 为汇点
  • 对于集合 $S,T,T\subseteq S$,约定 $S-T$ 为从 $S$ 中删掉 $T$ 中每个元素之后的集合
  • 网络流图为 $G=(V,E)$,边 $(u,v)$ 容量记为 $c_{u,v}$

最大流最小割定理和增广路定理

这两个定理是网络流问题的核心定理。

内容

最大流 = 最小割。

残量网络中不存在增广路是当前流为最大流的充要条件。

证明

  1. 若当前流为最大流,显然不存在增广路。

  2. 若当前流等于某个割,显然当前流为最大流,且该割为最小割。

  3. 若不存在增广路,我们证明当前流等于一个割。

令 $S=\{v,\exist\ p_{s,v}\}$,$S$ 即 $s$ 在残量网络中能到达的点的集合。令 $T=V-S$。显然 $(S,T)$ 是一个割,对于当前残量网络 $G’=(V,E’)$,一定有 $\forall x \in S,\forall y\in T$,边 $(x,y)$ 满流,否则 $y\in S$。容易证明当前流恰好为 $S$ 到 $T$ 的所有边的容量和,也就是割的大小。因为此时原图 $S$ 到 $T$ 所有的边流量为容量,$T$ 到 $S$ 的边流量为 $0$,所以流量为 $S$ 到 $T$ 的边的流量之和,也就是割 $S,T$。

算法

Dinic

考虑对残量网络 bfs 分层,强制流量只能流向下一层,再进行一次 dfs,求限制下所有的增广路,搞定。

复杂度 $O(n^2m)$

每次增广复杂度为 $O(nm)$,共计增广 $n$ 次,因为每次增广都会让 $dep[t]$ 增加 1。

每次增广的复杂度不是很好证,但加了当前弧优化其实就很松。

代码记在脑子里了,不放了。

ISAP

咕咕咕

HLPP

咕咕咕

费用流相关

一般指最小费用最大流,暂时没啥感觉,不写。

最小割

非常常见的一个网络流模型应用。

核心思想

通过构造一个图的割到决策方案的映射,其中决策必须可拆分计算,求出最小的代价,一般来说决策的限制如果比较奇怪就应该考虑最小割。

常用于规划 $0/1$ 独立贡献决策问题。

适用条件

  • 不能存在负容量边权,我不知道有没有人会,反正我是不会。

1.最大闭合权子图问题

给定一张有向图,点带权(权可以为负),选一个子图出来,要求如果 $u$ 选了那如果存在 $(u,v)$,那 $v$ 也要选。求最大权值和。

决策贡献独立,决策类型为 0/1。应该可以最小割。令割中与 $s$ 同集合的点为选择的点,与 $t$ 同一个集合的点为未选择的点。那么每个点应该和 $t$ 连一条流量为权值的相反数的边,代表选它的代价,再对于每条 $(u,v)$ 从 $u$ 向 $v$ 连一条 inf 边,代表选了 $u$ 不选 $v$ 的代价。考虑这张图的一个最小割,它必然不会包含 inf 边,也就是说,我们选了 $u$ 在 $s$ 中一定会让 $v$ 在 $s$ 中,所以一定合法。然后发现原问题的每一个答案都可以和图上的一个不含 inf 边的割一一对应,故最小割就是原问题的答案的相反数。

这张图有负数,不行,所以考虑先默认选所有正数。那么选负数的代价为相反数,不选正数的代价为本身。

那么应该从 $s$ 向正权点连边,从负权点向 $t$ 连边,最后对于 $(u,v)$ 从 $u$ 向 $v$ 连 inf 边即可。

总结一些科技

主要收录比较神仙的,实用的算法技巧。

快速取模

原理

找到一个近似 $m^{-1}$ 的形如 $m’>>k$ 的数。

不妨就取 $k=64,m’=\lceil\frac{2^{64}}{m}\rceil$

然后 $a\%b = a-a\times\lfloor\frac{a}{b}\rfloor = a-(a\times m’>>64)$

纯纯的整数运算,经过误差分析,可以知道后式结果最多多减去一个 $m$,判断掉就行。

因为 $a$ 常常是 $\text{long long}$ 级别的数,所以开 __int128

优化据说有 $5-6$ 倍,如果模数是 const,编译器会自动帮忙用这个优化。

代码

1
2
3
4
5
6
7
8
9
10
11
12
struct barrett{
unsigned long long im;int m;
barrett(unsigned m) :m(m), im(~0ull/m+1) {}
int operator ()(int a,int b){
unsigned long long z=(unsigned long long)a*b;
int v=z-((__int128)z*im>>64)*m;
return v<0?v+m:v;
}
}bt(1);
bt=barrett(p);
c=bt(a,b);
//c=a*b%p

注意事项

  • 为啥 im,mull,uint,因为 m=2 时,会爆 long long
  • 可以重载括号。

光速乘

用于计算两个 long long 级别的数乘积对一个 long long 级别的数取模的结果。

1
2
3
4
long long multi(long long a,long long b,long long mod){
long long x=(unsigned long long)a*b-(unsigned long long)((long double)a*b/mod-0.5)*mod;
return x>=mod?x-mod:x;
}
1
2
3
4
long long mul(long long A, long long B, long long P){
long long C = A * B - (long long)((long double)A * B / P + 0.1) * P;
return C < 0 ? C + P : C;
}

原理很简单,long double 的精度误差虽然有,但是我们 -0.5 之和核查范围变成了 [0,1],肯定不会超出这个。

C++ 标准中要求了 unsigned long long 类型在溢出后保证为原值对 $2^{64}$ 取模的结果,所以直接用就行。

第二个原理类似,在 G++ 编译器中,O2 中,保证 long long 的溢出行为有定义。

推荐用第一个,各个平台都不会出锅。

如果 $P\leq 10^{14}$ 可以改成 double

判断一下是否减少了就行。