2019学军集训游记
Day0 12.7
昨天度过了地狱一般的段考,早上开始出发去机场,然后坐飞机到了杭州。
表示自己以前只做过两次飞机。所以这是一次新奇的体验。
酒店比以前要烂,但是价钱便宜。
然后到了学军中学。
拿到账号,登录XJOI。
然后发现JZOJ比XJOI高到不知道哪里去了。
Day1 12.8
第一天就自闭了。
第一题,给你平面直角坐标系的\(N\)个\(x\)轴上的点和\(M\)个\(x\)轴下的点,还有\(K\)个在\(x\)轴上平行于\(x\)轴的线段。
对于每个\(x\)轴下面的点,询问它到\(x\)轴上面的点的连线满足连线不穿过任何线段的\(x\)轴上面的点的个数。
\(N,M\leq1e5 \ K\leq 50\)(部分测试数据强制在线)
比赛时乱搞了一下,大概思路是:
对于每个上面的点,向所有的线段的端点连线,差分(比赛时我似乎还忘记判重)。
离线(强制在线的数据不要了),用某条平行于\(x\)轴的直线从上到下扫过去,维护这个直线于上面的点和线段端点的交点的相对顺序。
打完之后样例没有过,调试的过程中发现我之前分析的时间复杂度是伪的……
不得不打暴力。
最后突然发现了新天地:有个保证所有纵坐标相同的子任务,并且强制在线!
然后我就搞了个大水法:对于强制在线的数据,我们假装纵坐标相同,那么就可以通过它输入的数异或上真正的纵坐标得出答案。这样,就只需要暴力求出最后一个询问的答案。
这个数据有\(15\)分,然而我比赛时少打了个m--
,于是这\(15\)分就没了……
【正解】
大概是对于每个\(x\)轴上的点,对每个线段的端点连线,差分,极角排序,去重。
将这些直线挂在线段的端点上(按照经过的端点分类),极角排序,维护前缀和。
对于每个询问,枚举每个线段的端点,二分出它所在的区域,加上这个区域的前缀和。
第二题,给你一个数列,然后有一堆询问,每次询问区间\([l,r]\)的所有子区间的\(mex\)和。
\(n<=1e6\)
求\(mex\)是个比较经典的问题。普通的求\(mex\)就直接在权值线段树上维护最后出现位置的最小值,求的时候线段树上二分。
然而这题要求所有子区间这个答案的和。由于线段树二分维护的东西对于多个区间搞似乎不太好做,于是我就开始寻找新大陆。
然后新大陆寻找到我自闭了……
最终什么都没有打。
【正解】
说实在的,Cold_Chair的题解我看不懂……
了解了大概思想之后自己钻研。
搞来搞去,一直到12.11才AC。
按照套路,枚举右端点\(i\),维护每个\([l,i]\)的\(mex\)值。
假如我们可以维护这个东西,那么询问所有子区间就可以求历史版本的和。
首先,求历史版本的和是个老问题,之前JZOJ6429就做过。
那么现在的问题是如何维护\(mex\)值。
首先,很显然的一点是,对于一集合\(S\),如果\(s\)是\(S\)的子集,那么\(mex(S)\geq mex(s)\)
那么很显然\(l\)从左到右是递减的。我们可以把它看成一个单调栈。
考虑从右边加入一个数后的影响(记作\(a_i\)):
如果这个数在\(mex\)的序列中没有出现,那么它对这个序列没有影响。
如果这个数在\(mex\)的序列中有出现呢?
我一开始认为是将\(mex\)为\(a_i\)的区间找出来,然后赋值前面的\(mex\)。
然后我发现这是错误的……
比如这个美丽的数据:
Input:
0
5
2 0 2 1 0
2
4 5
3 5
Output:
3
6
原因是,假如对于某个点\(y\),存在\(z\)使得\(x\leq z <y\)(\(x\)为\(mex\)为\(a_i\)区间的左端点),并且\(a_z\in S_y\)不成立(\(S_y\)表示\([y,i]\)中的数形成的集合),并且\(a_i<a_z<mex(S_{x-1})\),那么在加入\(a_i\)后,\(mex(S_y)\)可能变成\(a_z\)。
所以我稍微转换了一下模型:
对于每个权值(记作\(x\)),求出它最后出现的位置\(y\),那么相当于要将区间\([y+1,i]\)和\(x\)取\(min\)。
在后面加入值\(a_i\)以后,\(a_i\)的最后出现位置就变了。假如我们将区间取\(min\)操作视作覆盖,那么这就相当于是:将\(a_i\)的覆盖给撤去,这样一些新的区间就会暴露出来。
注意到每个区间最多会暴露一次,暴露过后不会再被覆盖,顶多是被撤去。
不考虑区间右端点,我们发现不同的区间个数是\(O(n)\)的。所以完全可以暴力处理这些区间。
具体的做法是:假如加入了\(x\),\(lst\)为上一次出现的位置,那么我们找到满足\(x0<x\)并且\(lst_{x0}>lst_x\)的\(lst_x\)最小的\(x0\)。那么\([x+1,x0]\)的\(mex\)变成\(mex(S_x)\),而以\(x0+1\)为开头的某个区间就被“暴露”出来了。接着将\(x\)赋值为\(x0\),继续暴力就行了。
时间复杂度当然是\(O(n \lg n)\)的
然而代码复杂度巨大:打三个线段树,一个求历史版本和,一个用于求某个权值最后出现的位置以及\(mex\),一个用于求某个位置后面第一个权值小于它并且出现的最后位置大于它的权值。
空间开销也挺大的,所以我将卡空间的家底都拿出来了。比如说堆式存储的线段树,强行将它的大小变成\(2\)的幂次,那么空间就只需要开到\(2n\)。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 1000010
#define MAX 1000000001
#define ll long long
int n,nN,a[N];
struct Func{
ll k,b;
};
Func sum[1<<21],tag[1<<21];
inline Func operator+(Func &x,Func &y){return {x.k+y.k,x.b+y.b};}
inline void operator+=(Func &x,Func y){x.k+=y.k,x.b+=y.b;}
inline Func operator*(Func &x,int y){return {x.k*y,x.b*y};}
void add(int t,int l,int r,int st,int en,Func c){
if (st<=l && r<=en){
sum[t]+=c*(r-l+1);
tag[t]+=c;
return;
}
int mid=l+r>>1;
if (tag[t].k || tag[t].b){
tag[t<<1]+=tag[t],tag[t<<1|1]+=tag[t];
sum[t<<1]+=tag[t]*(mid-l+1),sum[t<<1|1]+=tag[t]*(r-mid);
tag[t]={0,0};
}
if (st<=mid)
add(t<<1,l,mid,st,en,c);
if (mid<en)
add(t<<1|1,mid+1,r,st,en,c);
sum[t]=sum[t<<1]+sum[t<<1|1];
}
Func res;
void query(int t,int l,int r,int st,int en){
if (st<=l && r<=en){
res+=sum[t];
return;
}
int mid=l+r>>1;
if (tag[t].k || tag[t].b){
tag[t<<1]+=tag[t],tag[t<<1|1]+=tag[t];
sum[t<<1]+=tag[t]*(mid-l+1),sum[t<<1|1]+=tag[t]*(r-mid);
tag[t]={0,0};
}
if (st<=mid)
query(t<<1,l,mid,st,en);
if (mid<en)
query(t<<1|1,mid+1,r,st,en);
}
struct Node{
int l,r;
int mn;
} d[N*32];
int null,rt;
int cnt;
int newnode(){d[++cnt]={null,null,0};return cnt;}
int getlst(int t,int l,int r,int x){
if (t==null)
return 0;
if (l==r)
return d[t].mn;
int mid=l+r>>1;
if (x<=mid)
return getlst(d[t].l,l,mid,x);
return getlst(d[t].r,mid+1,r,x);
}
int getmex(int t,int l,int r,int st){
if (t==null || l==r)
return l;
int mid=l+r>>1;
if (d[d[t].l].mn>=st)
return getmex(d[t].r,mid+1,r,st);
return getmex(d[t].l,l,mid,st);
}
void insert(int &t,int l,int r,int x,int c){
if (t==null)
t=newnode();
if (l==r){
d[t].mn=c;
return;
}
int mid=l+r>>1;
if (x<=mid)
insert(d[t].l,l,mid,x,c);
else
insert(d[t].r,mid+1,r,x,c);
d[t].mn=min(d[d[t].l].mn,d[d[t].r].mn);
}
int X,Y;
int mn[1<<21];
void change(int k,int l,int r,int x,int c){
if (l==r){
mn[k]=c;
return;
}
int mid=l+r>>1;
if (x<=mid)
change(k<<1,l,mid,x,c);
else
change(k<<1|1,mid+1,r,x,c);
mn[k]=min(mn[k<<1],mn[k<<1|1]);
}
void find(int k,int l,int r,int x,int st,int en){
if (mn[k]>=x)
return;
if (l==r){
X=mn[k];
Y=l;
return;
}
int mid=l+r>>1;
if (st<=mid){
find(k<<1,l,mid,x,st,en);
if (!(X==-1 && Y==-1))
return;
}
if (mid<en)
find(k<<1|1,mid+1,r,x,st,en);
}
bool getnxt(int x,int y,int i){
X=-1,Y=-1;
find(1,1,nN,x,y+1,i-1);
return X==-1 && Y==-1?0:1;
}
int m;
struct Oper{
int l,r,num;
} o[N];
bool cmpo(Oper x,Oper y){return x.r<y.r;}
ll ans[N];
int main(){
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
scanf("%*d%d",&n);
nN=1;
for (;nN<n;nN*=2);
for (int i=1;i<=n;++i)
scanf("%d",&a[i]);
scanf("%d",&m);
for (int i=1;i<=m;++i)
scanf("%d%d",&o[i].l,&o[i].r),o[i].num=i;
sort(o+1,o+m+1,cmpo);
null=0;
d[null]={null,null,0};
rt=null;
for (int i=1,j=1;i<=n;++i){
change(1,1,nN,i,a[i]);
int lst=getlst(1,0,MAX,a[i]);
if (lst){
change(1,1,nN,lst,MAX+1);
if (lst!=i-1){
int x=getmex(rt,0,MAX,lst+1);
if (x==a[i]){
int y=lst,nw=getmex(rt,0,MAX,lst);
while (1){
if (getnxt(nw,y,i)==0?1:Y>=i-1){
if (y+1<=i-1)
add(1,1,nN,y+1,i-1,(Func){nw-x,-(ll)(i-1)*(nw-x)});
break;
}
add(1,1,nN,y+1,Y,(Func){nw-x,-(ll)(i-1)*(nw-x)});
if (getmex(rt,0,MAX,Y+1)==X)
break;
nw=X,y=Y;
}
}
}
}
else{
int x=getmex(rt,0,MAX,1);
if (x==a[i]){
insert(rt,0,MAX,a[i],i);
int y=0,nw=getmex(rt,0,MAX,1);
insert(rt,0,MAX,a[i],0);
while (1){
if (getnxt(nw,y,i)==0?1:Y>=i-1){
if (y+1<=i-1)
add(1,1,nN,y+1,i-1,(Func){nw-x,-(ll)(i-1)*(nw-x)});
break;
}
add(1,1,nN,y+1,Y,(Func){nw-x,-(ll)(i-1)*(nw-x)});
if (getmex(rt,0,MAX,Y+1)==X)
break;
nw=X,y=Y;
}
}
}
insert(rt,0,MAX,a[i],i);
int x=a[i]==0?1:0;
add(1,1,nN,i,i,(Func){x,-(ll)(i-1)*x});
for (;j<=m && o[j].r==i;++j){
res={0,0};
query(1,1,nN,o[j].l,i);
ans[o[j].num]=res.k*i+res.b;
}
}
for (int i=1;i<=m;++i)
printf("%lld\n",ans[i]);
return 0;
}
第三题,给你一个数列,两个数字相同表示种类相同。
两个子串相同定义为两个子串最小表示法之后的形式相同。
问不同的子串个数。
\(n\leq 50000\)
按照套路,求出每个位置的后面和它种类相同的位置与它的差。
然而,我不会处理越界的情况……
自闭,于是直接打了哈希的大暴力。
【正解】
先不要想这种作差的方法。考虑将所有的后缀用最小表示法搞出来,后缀排序(用二分加哈希),子串个数减去两两之间\(LCP\)的长度就是答案。
接下来直接将它变成作差的方法。可以用主席树来搞出右端点为多少的时候,每个后缀的哈希值。
这样二分加哈希的时候查历史版本就可以比较了,因为这个时候不用考虑一个出界、一个不出界的情况。
Day2 12.9
比起昨天,今天的题目似乎友善了许多。
第一题:
给你\(n\)个\(m\)位二进制数,取其中任意的数异或起来。得出的数如果有一位\(i\)为\(1\),那么这一位就有着\(a_i*3^{b_i}\)的贡献。
其中\(a_i \in {-1,1}\)且\(b_i \in {3^k|1\leq k \leq 35}\)
求贡献的最大和。
其中\(n\leq 1e5,m\leq 70\),并且保证没有相同的\((a_i,b_i)\)
这个题目有个很显然的性质:取的位的贡献越大越好(假如不取它的相反数)。也就是说,就算有另一种方案,使得贡献为正数并且所有贡献小于它的位都取到,而且取它这位的时候贡献为负数且贡献绝对值小于它的位都被取到,那么还是取这一位更优秀一些。
所以肯定是先处理贡献绝对值最大的位。
假如它的相反数不存在,那就根据它的正负来钦定它选或不选,钦定的时候判断一下是否成立。
假如相反数存在,那就试着选正数、不选负数;不行就试着同时选或不选它们;再不行就选负数,不选正数。
想到这里我就自闭了,因为我不知道怎么判断它是否成立啊……
【正解】
线性基。线性基?线性基!
之前没有听说过这个东西,上网一查,网上有着各种各样的神仙解释。
说人话,实际上线性基就是一堆二进制数,通过高斯消元后将全部为\(0\)的列去掉后的东西。
强行学习线性基,晚上切掉了两道线性基的裸题。
然后开始刚这一题。
这一题当然就是个线性基的裸题。在添加限制的时候,将线性基中进行一波消元,然后看看能不能满足钦定的那些点。
现在最主要的问题是如何搞相同的情况。
实际上,相同的情况就是两者异或起来等于\(0\)的情况。那么就可以将这两列合并在一起,然后钦定它为\(0\)。
具体怎么合并呢?我的做法是先按照贡献绝对值排好序,合并的时候将其中的一列变成异或值,另一列变成\(0\),打个标记忽略它。
%%%DYP大佬暴力重构。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#include <climits>
#include <bitset>
#define ll long long
#define N 200010
#define M 81
int n,m,a[M],b[M];
ll pow3[40];
char str[M];
bitset<70> mat1[M],mat[M];
bool bz[M];
void insert(bitset<70> mat[],bitset<70> &s){
for (int i=m-1;i>=0;--i)
if (!bz[i] && s[i]){
if (mat[i][i]==0){
mat[i]=s;
return;
}
s^=mat[i];
}
}
int yd[M];
bitset<70> s;
bool judge(bitset<70> mat[]){
s.reset();
for (int i=m-1;i>=0;--i){
if (bz[i])
continue;
if (yd[i]!=-1 && s[i]!=yd[i]){
if (mat[i][i])
s^=mat[i];
else
return 0;
}
}
return 1;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
// freopen("matrix.in","r",stdin);
// freopen("matrix.out","w",stdout);
scanf("%d%d",&n,&m);
for (int i=0;i<n;++i){
scanf("%s",str);
s.reset();
for (int j=0;j<m;++j)
s[j]=str[j]-'0';
insert(mat1,s);
}
for (int i=0;i<m;++i)
scanf("%d%d",&a[i],&b[i]);
for (int i=0;i<m;++i)
for (int j=i+1;j<m;++j)
if (b[j]<b[i] || b[j]==b[i] && a[j]<a[i]){
swap(a[i],a[j]);
swap(b[i],b[j]);
for (int k=0;k<m;++k){
int t=mat1[k][i];
mat1[k][i]=mat1[k][j];
mat1[k][j]=t;
}
}
for (int i=0;i<m;++i)
insert(mat,mat1[i]);
ll ans=0;
pow3[0]=1;
for (int i=1;i<40;++i)
pow3[i]=pow3[i-1]*3ll;
memset(yd,255,sizeof yd);
for (int i=m-1;i>=0;--i){
if (i>1 && b[i]==b[i-1]){
yd[i]=1,yd[i-1]=0;
if (judge(mat))
ans+=pow3[b[i]];
else{
memcpy(mat1,mat,sizeof mat);
yd[i]=0;
bz[i-1]=1;
for (int j=0;j<m;++j){
mat[j][i]=mat[j][i]^mat[j][i-1];
mat[j][i-1]=0;
}
if (mat[i][i]==0 && mat[i]!=0){
s=mat[i];
mat[i].reset();
insert(mat,s);
}
if (mat[i-1][i-1]==0 && mat[i-1]!=0){
s=mat[i-1];
mat[i-1].reset();
insert(mat,s);
}
if (!judge(mat)){
memcpy(mat,mat1,sizeof mat1);
bz[i-1]=0;
yd[i]=0,yd[i-1]=1;
ans-=pow3[b[i]];
}
}
--i;
}
else{
if (a[i]==1){
yd[i]=1;
if (judge(mat))
ans+=pow3[b[i]];
else
yd[i]=0;
}
else{
yd[i]=0;
if (!judge(mat))
ans-=pow3[b[i]],yd[i]=1;
}
}
}
printf("%lld\n",ans);
return 0;
}
第二题
给你一棵有根树,能够修改边的权值,修改的时候有不同的代价。每条边修改的代价和它修改前后之差的绝对值成正比。
问使得根节点到叶子节点权值和相同需要花费的最小代价。
\(n \leq 200000\)
这道题一看就是道神仙题。
搞了好久,最终只是交了个普通的DP做法,预计能有20分。
然而出来之后,发现自己打挂了。
【正解】
不是很记得。
大概是\(DP\)时的状态可以设为一堆一次函数,用平衡树来维护凸包……
第三题
给你一棵带边权树,让你对于每个节点定义一个\(m\)维向量,使得两两点之间,同样维度上的坐标之差的绝对值的最大值等于它们在树上的距离。
\(n\leq 1000\)
\(m\)是自己求出来的,\(m\leq 16\)时有满分。
这题一开始是懵逼了半天,后来渐渐地想到了边分治。
边分治将树分成两个部分,然后设置权值使得一边的点能到达另一边。
那就在边的其中一个方向对点做前缀和,另一个方向做负的前缀和。
然后我长时间地纠结在一个问题上:如何保证新的权值之差小于等于两点之间的距离呢?
【正解】
正解是点分治,不过我认为跟边分治其实没有太大的区别。
这个点分治比较奇怪:找出重心后,将子树分成两个尽量平均的部分,每个部分都带上重心。
分成两个尽量平均的部分就用贪心:将所有的子树按照大小排序,设两个集合\(A\)和\(B\),初始的时候都是空集。从大往小做,将当前的子树分配到比较小的集合中去。
点分治的时间复杂度是可以保证的,每次联通块至少减少\(\frac{1}{3}\)个点。
证明:
设当前的集合大小为\(A\)和\(B\),现在要加入点\(x\)。钦定\(A\leq B\)
归纳证明,\(A \geq \frac{A+B}{3}\)
于是\(A+x\geq \frac{A+B}{3}+x\geq \frac{A+B+x}{3}\)
由于\(x\leq A \leq B\)
所以\(2B\geq A+x\),得到\(B\geq \frac{A+B+x}{3}\)
大佬说,只要满足分成的两个联通块在相交的权值相同,那么就一定满足条件。
这个东西是可以证明的。
在两个联通块的权值之后,根据它们在相交处的权值调整一下它们的权值就行了。
然而实际上并不简单,因为联通块是可以跟多个其它的联通块相交的……
于是这种打法就比较繁琐。
接着我发现我们其实只需要钦定边权,一条边(有向)表示这条边出发点的权值加多少等于终点的权值。(注意钦定的时候还要处理一下反向边)
容易发现边是不相交的。在点分治完一层之后,随便找个点做前缀和就可以了。
然后,你就会发现有一种关于其正确性的一种非常优美的证明方法:
实际上,我们可以发现,对于两个点之间的路径,上面的每条边的权值的绝对值都是和题目给定的一样的。
假如权值全都取正数,那么权值和一定是最大的。这个也是它们实际的距离。
现在这样子处理使得权值有正有负,那么权值和自然没有实际的距离大。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cassert>
#define N 1010
int n;
struct EDGE{
int to,w;
EDGE *las;
} e[N*2];
int ne;
EDGE *last[N];
int len[N*2];
#define rev(ei) (e+(int(ei-e)^1))
int dx[N][20],cntd;
vector<int> dot[N*20];
int kill[N*20];
int tail;
int fl[N*20];
bool bel[N];
int lis[N];
int siz[N];
void getsiz(int x,int fa){
siz[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (bel[ei->to] && ei->to!=fa)
getsiz(ei->to,x),siz[x]+=siz[ei->to];
}
int getg(int x,int fa,int all){
int mx=all-siz[x];
for (EDGE *ei=last[x];ei && mx<=all>>1;ei=ei->las)
if (bel[ei->to] && ei->to!=fa)
mx=max(mx,siz[ei->to]);
if (mx<=all>>1)
return x;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (bel[ei->to] && ei->to!=fa){
int tmp=getg(ei->to,x,all);
if (tmp)
return tmp;
}
return 0;
}
bool cmpl(int a,int b){return siz[e[a].to]>siz[e[b].to];}
void put(int x,int fa,int num){
dot[num].push_back(x);
for (EDGE *ei=last[x];ei;ei=ei->las)
if (bel[ei->to] && ei->to!=fa)
put(ei->to,x,num);
}
void setdir(int x,int fa,int o){
for (EDGE *ei=last[x];ei;ei=ei->las)
if (bel[ei->to] && ei->to!=fa){
len[ei-e]=ei->w*o;
len[(ei-e)^1]=-ei->w*o;
setdir(ei->to,x,o);
}
}
void getdx(int x,int fa,int num){
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa){
dx[ei->to][num]=dx[x][num]+len[ei-e];
getdx(ei->to,x,num);
}
}
int dis[N][N];
int main(){
freopen("in.txt","r",stdin);
scanf("%d",&n);
if (n==1){
printf("0\n0\n");
return 0;
}
for (int i=1;i<n;++i){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
e[ne]={v,w,last[u]};
last[u]=e+ne++;
e[ne]={u,w,last[v]};
last[v]=e+ne++;
}
for (int i=1;i<=n;++i)
dot[1].push_back(i);
fl[tail=1]=1;
bool con=0;
for (int i=1;i<=tail;++i){
cntd=max(cntd,fl[i]);
int k=dot[i].size();
for (int j=0;j<k;++j)
bel[dot[i][j]]=1;
getsiz(dot[i][0],0);
int g=getg(dot[i][0],0,k);
getsiz(g,0);
int ns=0;
for (EDGE *ei=last[g];ei;ei=ei->las)
if (bel[ei->to])
lis[ns++]=ei-e;
sort(lis,lis+ns,cmpl);
dot[tail+1].push_back(g);
dot[tail+2].push_back(g);
int A=0,B=0;
for (int i=0;i<ns;++i)
if (A<=B){
A+=siz[e[lis[i]].to];
put(e[lis[i]].to,g,tail+1);
len[lis[i]]=e[lis[i]].w;
len[lis[i]^1]=-e[lis[i]].w;
setdir(e[lis[i]].to,g,1);
}
else{
B+=siz[e[lis[i]].to];
put(e[lis[i]].to,g,tail+2);
len[lis[i]]=-e[lis[i]].w;
len[lis[i]^1]=e[lis[i]].w;
setdir(e[lis[i]].to,g,-1);
}
fl[tail+1]=fl[tail+2]=fl[i]+1;
tail+=2;
con|=(A>=2||B>=2);
for (int j=0;j<k;++j)
bel[dot[i][j]]=0;
if (fl[i]!=fl[i+1]){
getdx(1,0,fl[i]);
if (con==0)
break;
con=0;
}
}
memset(dis,63,sizeof dis);
for (int i=1;i<=n;++i)
dis[i][i]=0;
for (int i=1;i<=n;++i)
for (EDGE *ei=last[i];ei;ei=ei->las)
dis[i][ei->to]=ei->w;
printf("%d\n",cntd);
for (int i=1;i<=n;++i,printf("\n"))
for (int j=1;j<=cntd;++j)
printf("%d ",dx[i][j]);
return 0;
}
Day3 12.10
这天没有比赛,在学军好好改题。
然而改了一天也没有改多少
Day4 12.11
第一题
给你一棵树,每个节点上有个线段(不与\(y\)轴平行)。
然后有一堆询问,每次询问一条路径上的所有点在横坐标为\(x\)时的最大值(如果没有就为\(0\))
\(n\leq 1e5\),\(Q\leq 1e5\)
\(0<k,l,r,x\leq 1e6 b\leq 1e12\)
这道题显然是一道数据结构题对吧……
搞了半天,想出了一个\(O(n \sqrt(n) \lg n)\)的方法,感觉似乎可以AC!
首先树链剖分,然后在重链上以根号长度为块的大小分块。
对于每个块,维护个线段树,维护的是某个横坐标上的最大值。具体来说,就是向线段树分治那样,将线段所在横坐标的区间挂在线段树上完全覆盖的最大区间上,然后对于每个线段树上每个节点维护一个凸包。
这样看上去,时间复杂度是\(O(n \sqrt(n) \lg^2 n)\)的。但实际上,由于分块是按照重链的长度来分的。根到某个节点的路径上的重链的长度每次至少减半。所以那个表示重链个数的\(\lg n\)是没有的。
但实际上这样打常数特别大,而且更加不爽的是,我比赛时打挂了,直到比赛之后才调出来。调出来之后发现只有\(37\)分。卡了卡常数,变成了\(63\)分,然后就卡不动了……
【正解】
这题的做法似乎特别多,\(O(n\lg^3 n)\)和\(O(n \sqrt(n) \lg n)\)的就有一大片。
正解在理论上是\(O(n \lg n)\)
实现的方式比较清真,维护一个区间内的线段合并出来的一堆在最上面的性质。
这个合并是非常暴力的,就是用你能想到的最暴力的方式合并。(不过似乎有些复杂的分类讨论)
大佬证明合并之后线段总数是\(O(n)\)级别的。
于是这道题基本上就没了。把整棵树剖一遍,每条重链维护这样的东西,\(O(n \lg^2 n)\)。
搞个全局平衡二叉树(实际上似乎没有什么卵用),理论上\(O(n \lg n)\)
第二题
给你个\(01\)矩阵,让你求对于每个位置,位置上的值反转之后矩阵的秩。
\(n,m\leq 3000\)
完全没有思路……
正解太恐怖,完全听不懂。
第三题
多重背包问题,强制你选\(k\)种物品(\(k\in S\),\(S\)为),问最终你能得到哪些小于\(L\)的总体积值。
\(n,c_i,v_i,L\leq 2000\)
比赛时一个纯暴力了事……
【正解】
正解似乎挺简单。
将DP数组形成的矩阵画出来,就可以脑补一下它转移时的样子。
发现是前面的某一列整体下移一位之后和当前这一列取或。
用类似于单调队列维护背包的方法来搞,用bitset
压一下位。
就可以达到\(O(n^3)\)
注意:由于用\(bitset\)压位之后不好搞前缀和,就可以每隔\(v_i\)个放一个观测点,往两边求前缀和后缀和。
Day5 12.12
第一题
给你正整数\(n\)和\(k\)和\(l\),让你求\(l\)串长度为\(n\)的,有\(k\)个\(1\)的\(01\)序列,满足:
相邻的序列可以通过将上一个序列的其中一个\(1\)右移一位得到。
相邻的序列循环同构。
让你构造这些序列。
\(n,l\leq 100\)
看到这题的时候,我以为是满足两个条件中任意一个,所以爆0了。
【正解】
正解其实很巧妙。
可以用相邻的\(1\)之间的相对距离来表示这个\(01\)序列。
将一个\(1\)右移,意味着将它到上一个\(1\)的距离加一,到下一个\(1\)的距离减一。
由于它们循环同构,所以这个相对距离表示的序列也循环同构。
我们要保持相对距离为某个数的个数不变。
那么我们可以这样构造:若干个相对距离为\(\lfloor\frac{n}{k}\rfloor\)和若干个相对距离为\(\lfloor\frac{n}{k}\rfloor+1\)
如果将相对距离为前者用\(0\)来表示,后者用\(1\)来表示,就能形成一个长度为\(k\)的,有\(n \mod k\)个\(1\)的\(01\)序列。
原序列中某个\(1\)右移的时候,相当于这个序列中这个\(1\)对应的位置加一,后面的减一,也就相当于后面有个\(1\)左移了。
然后就可以发现这其实是个子问题!
递归下去做,就可以构造出这个序列了。
现在问题是如何知道当前序列,搞到下一个序列。
注意到当前序列向右移的时候,相当于递归下一层的区间左移,相当于递归下下层的区间右移……那么递归到最底下的区间,左移或右移之后回溯,根据下一层的改变情况来推算出当前该移动位置。注意,移动的时候钦定它往某个方向移动。
(数据这么小,其实直接暴力地算出下一个是哪一个就行了……)
代码
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 110
int n,K,L;
struct Stack{
int n,k;
} st[N];
int f[N][N*2],w[N][N],c;
int p;
int dir;
int top;
void dfs(int x){
if (x==top){
if (dir==1){
c=p;
f[x][p]=0;
f[x][++p%=st[x].n]=1;
}
else{
f[x][p]=0;
f[x][(p+=st[x].n-1)%=st[x].n]=1;
c=p;
}
return;
}
dir*=-1;
dfs(x+1);
dir*=-1;
int a=st[x].n/st[x].k;
if (dir==1){
int tmp=w[x+1][c];
f[x][w[x+1][c]]=0;
++w[x+1][c]%=st[x].n;
f[x][w[x+1][c]]=1;
c=tmp;
}
else{
f[x][w[x+1][c]]=0;
(w[x+1][c]+=st[x].n-1)%=st[x].n;
f[x][w[x+1][c]]=1;
c=w[x+1][c];
}
}
void solve(){
top=0;
for (int i=n,j=K;i;){
st[++top]={i,j};
if (i%j==0){
if (j!=1){
printf("NO\n");
return;
}
break;
}
int t=i%j;
i=j,j=t;
}
printf("YES\n");
for (int i=0;i<L;++i){
memset(f[top],0,sizeof(int)*st[top].n);
f[top][0]=1;
for (int j=top-1;j>=1;--j){
int k=0,a=st[j].n/st[j].k;
for (int t=0;t<st[j+1].n;++t){
for (int l=0;l<a+f[j+1][t]-1;++l)
f[j][k++]=0;
w[j+1][t]=k;
f[j][k++]=1;
}
}
}
p=0;
for (int i=0;i<L;++i){
for (int i=0;i<n;++i)
putchar(f[1][i]+'0');
putchar('\n');
dir=1;
dfs(1);
}
}
int main(){
int T;
scanf("%d",&T);
while (T--){
scanf("%d%d%d",&n,&K,&L);
if (K==0 || n<=K){
printf("NO\n");
continue;
}
solve();
}
return 0;
}
第二题
给你\(n\)个长度不超过\(50\)的字符串,让你将每个字符串取出一个前缀,拼在一起,使得字典序最小。
\(n\leq 50\)
比赛的时候刚了好久,想到了一个难以用语言描述的做法,但是不敢打。
【正解】
不会……
第三题
给你一棵树,每个节点上有权值表示收益,每条边有权值表示打通这条边的代价。
现在要打通到某个节点,求一开始最小的钱使得中间钱数不会小于\(0\)。
\(n\leq 200000\)
比赛时大部分时间都在刚第二题,后来意识到这题似乎做过一道很类似的……
不过在比赛之后才发现,其实也并不是一模一样……
【正解】
正解很巧妙。
将目标节点上的收益设为无限大。
考虑打通所有的边,走过所有节点的一开始最小的钱。
这个问题和题目是等价的,因为如果找到某种方案到达目标节点,到达剩下节点的花费就不用愁了。
现在我们考虑的是打通边顺序的问题。
假如钱可以取到负数,那么这个问题等价于找到某种方案使得所有前缀和的最小值最大。
假设到达某个点付出的代价为\(a\),收益为\(b\),那么总共得到的收益为\(b-a\)。我们贪心地选取\(b-a\geq 0\)的,并且在这当中,先选\(a\)最小的。
将每个点的这些信息都丢进一个数据结构里。每次取出一个\(a\)最小的,然后和它的父亲合并。因为已经钦定了要选它了,所以如果选了它的父亲,就会立即选它。这个信息是可以合并的。
这样做一遍之后,根节点的代价就是答案。
代码:
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#define N 200010
#define ll long long
int n;
int fa[N];
ll a[N],b[N];
struct State{
int a,x;
};
bool operator<(State x,State y){return x.a<y.a || x.a==y.a && x.x<y.x;}
set<State> s;
int dsu[N];
int getfa(int x){return dsu[x]==x?x:dsu[x]=getfa(dsu[x]);}
int main(){
scanf("%d",&n);
for (int i=1;i<n;++i){
scanf("%d%lld%lld",&fa[i],&b[i],&a[i]);
if (b[i]==-1)
b[i]=1e18;
b[i]-=a[i];
if (b[i]>=0)
s.insert({a[i],i});
}
for (int i=0;i<n;++i)
dsu[i]=i;
while (!s.empty()){
int x=s.begin()->x;
s.erase(s.begin());
int f=getfa(fa[x]);
if (a[f]<a[x]-b[f]){
if (f && b[f]>=0)
s.erase(s.find({a[f],f}));
a[f]=a[x]-b[f];
b[f]+=b[x];
if (f && b[f]>=0)
s.insert({a[f],f});
}
else{
b[f]+=b[x];
if (f && b[f]>=0 && b[f]-b[x]<0)
s.insert({a[f],f});
}
dsu[x]=f;
}
printf("%lld\n",a[0]);
return 0;
}
Day6 12.13
第一题
给有一堆询问,每次给你三个整数\(n,m,a\),求\(\sum_{i=1}^n\sum_{j=1}^m\gcd(i,j)[\gcd(i,j)\leq a]\)
\(T\leq 100000,n,m,a\leq 100000\)
这题是我第一次在考场的时候就AC的题目……
【正解】
随便反演一波就是了。
某条式子跟\(a\)的取值有关系,把\(a\)离线,从小到大依次加一。
每次加一的时候,除以\(a\)下取整的区间加的东西都是一样的。
用个数据结构区间修改即可。(其实如果不用树状数组就会被卡常)
容易证明时间复杂度为\(n \sqrt n \ln n\)
第二题
给你一个联通平面图,然后有删边并询问联通块个数、询问两点是否在同一联通块两种操作。
强制在线。
\(n\leq 1e5,q\leq 2e5\)
看到这个,我一下子就想到了动态图……
可惜是强制在线的,我不会……
后来我试着挖掘一下平面图的性质,搞出了些奇奇怪怪的东西,但是最终什么都没有做……
仅仅是打了个最简单的暴力……
【正解】
首先,平面图转对偶图……
平面图上两个联通块隔离开来,相当于对偶图上多出了一个环。
用并查集判断一下就可以搞出第一问了。
至于第二问,在每次联通块分裂的时候启发式分裂,暴力染色。
所谓启发式分裂,实际上就是启发式合并反过来,时间复杂度是有保证的。
启发式分裂的时候·,两个联通块同时遍历,先遍历完的那个就是小的联通块。
第三题
你要从\((0,0)\)走到\((n,m)\),每次只能向右走和向上走。
记路径和\(y=0\)和\(x=n\)围成的封闭图形面积为\(k\),那么就会有\(q^k\)的贡献。
求所有贡献的和模\(p\)。
\(p,q\leq 2e5\),\(p\)和\(q\)是质数。
\(n,m\leq 1e9\)
比赛时时间不够,于是匆匆打了个\(40\)分的矩阵乘法……
【正解】
有个很牛的结论:答案为\(\frac{F(n+m)}{F(n)F(m)}\),\(F(n)=\prod_{i=1}^n(q^i-1)\)
这个东西可以归纳证明……其实只要发现了这条东西,证明还是很好证的。
然而它不能直接套公式,因为可能有模\(p\)为\(0\)的情况。
设\((q^k-1)\mod p=0\)。由于\(p\)和\(q\)不大,所以可以暴力求出来。
对于上面和下面的式子,计算\(p\)的因子个数和其它因子的积模\(p\)。
计算\(p\)因子的个数可以将\((q^k-1)\)提出来。提出来之后计算它的成绩,就能变成若干个\(p\)相乘再乘上某个阶乘的样子。
比较一下分数上下的\(p\)因子的个数,如果不等,那答案就是\(0\),否则计算一下,特意不要计算\(p\)因子就好了。
代码
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define ll long long
#define N 200000
ll n,m,q,p;
ll fac[N+10];
ll lis[N+10],r;
ll pro[N+10];
ll qpow(ll x,ll y){
ll res=1;
for (;y;y>>=1,x=x*x%p)
if (y&1)
res=res*x%p;
return res;
}
ll bfac(ll x){
if (x<p)
return fac[x];
return qpow(fac[p-1],x/p)%p*fac[x%p]%p*bfac(x/p)%p;
}
pair<ll,ll> F(ll n){
ll x=1,y=0;
x=qpow(pro[r-1],n/r)*pro[n%r]%p;
y=n/r+(n/r)/p;
// x=x*qpow(fac[p-1],n/r/p)%p*fac[n/r%p]%p;
x=x*bfac(n/r)%p;
return {x,y};
}
int main(){
int T;
scanf("%d%lld%lld",&T,&q,&p);
fac[0]=1;
for (int i=1;i<p;++i)
fac[i]=fac[i-1]*i%p;
lis[r++]=1;
while (lis[r-1]*q%p!=1){
lis[r]=lis[r-1]*q%p;
r++;
}
pro[0]=1;
for (int i=1;i<r;++i)
pro[i]=pro[i-1]*(lis[i]-1)%p;
while (T--){
scanf("%lld%lld",&n,&m);
pair<ll,ll> a=F(n+m),b=F(n),c=F(m);
(b.first*=c.first)%=p;
b.second+=c.second;
if (a.second>b.second)
printf("0\n");
else{
// assert(a.first && b.first);
ll res=a.first*qpow(b.first,p-2)%p;
printf("%lld\n",res);
}
}
return 0;
}
Day7 12.14
今天是pty出的题。
众所周知,pty出的题都很毒瘤。
第一题
有\(n\)个时间段,每个时间段可以做两件事情中的一件事情,分别有一定的收益。
对于每个长度为\(k\)的区间,要满足做的某一个事情的次数不小于一个定值(两种事情的定值不一定相同)。
问最大收益,并且给出方案。
\(k,n\leq 1000\)
比赛时磕了半天,一直到自闭……
【正解】
这道题是一道线性规划。
具体是怎样的,我听不太懂。
题目可以转化成你要选择序列上的若干个数,每个数可正可负,每个长度为\(k\)的区间内选择的数的个数在某个区间以内。问最大收益。
于是这就会形成若干条式子:用\(01\)表示选或不选,区间内的数加起来之后再加个非负数等于上界;下界同理。
搞完之后式子之间作差,就会搞出一个比较优美的东西来。
这时候就各种连边,跑费用流……
后面的我就不知道该怎么搞了……
第二题
你要求出含\(n\)个节点的不同无向图的个数,使得所有的极大边双都的大小都在给定的集合以内。
\(n,m\leq 100000\)
搞这题的时候我就连暴力都搞不出来……
打了个大表,蒙了几条DP式子,倒水了个位数的分。
【正解】
不会……
就是什么多项式之类的……
第三题
给你个网格图,经过每条边有一定的代价。
有很多标记过的网格,你要在用一个代价最小的环(可以自交),将这些标记过的网格围起来。
\(n,m\leq 400\)
【正解】
这题的正解没有用到特别复杂的知识点,但思路比较清奇。
首先,对于每个格点,可以拆成右上、左上、左下、右下四个点。他们连在一起形成一个环。方格内的四个点连上边。
枚举某个矩形,从某个矩形的左上角出发,跑最短路到其它的节点。
将最短路找出来:对于原图中的一条边,它两边分别有一条新图的边。在边的端点处,它们之间插着一条边本来使得它们联通,但你现在要将这条边删去。
形象化地讲,就是中间这条边将连接两边的中间这条边割断了。
对于一个矩形,由于最短路求的是到它左上角的最短路,所以有可能不包含它。那么,就将左上角的点在新图中的四个点之间的边割掉。
最后从某个节点左上角的点在新图中的右上方出发,跑一遍到左下方的最短路。
看起来是对的……
可能说得不太清楚,也有可能会出点问题,反正大概就是这样了。
Day8 12.15
这天被symbol安排去西湖。
所以整一天都在西湖那附近待着了。
话说我们遇见了西湖范大爷。
Day9 12.16
第一题
一棵带点权的树,有加边删边操作,求树上最长链。
\(n,m\leq 100000\)
这题一眼就知道是可以做的,然而比赛时出了些奇奇怪怪的错误。
我一开始用了multiset
,但却出现了些奇怪的错误。我调试的时候发现没有任何问题,估计它应该是内部出错了。
于是自己套了个线段树。
然而我没拍,所以就挂了……打LCT一定要拍……
【正解】
正解比赛时就想到了。
就是简单LCT。用mutliset
维护虚边的信息,然后有点像最大子段和那样维护。
线段树被卡了(尽管在本地跑的时候mutliset
打AC的程序永远没有我的程序快……)。
multiset
才能过,估计是因为每个点的度数不多,那个log
基本上是可以忽略不计的。
后来变回multiset
惊奇地发现那个bug不见了,于是从TLE80变成了100。
代码
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#define N 100010
#define MAX 1073741823
int n,m;
struct EDGE{
int to;
EDGE *las;
} e[N*2];
int ne;
EDGE *last[N];
struct Node *null;
struct Node{
Node *fa,*c[2];
bool rev,isr;
int val,sum,mxl,mxr,ans;
multiset<int> lt,mxa;
int flt,slt,fmxa;
inline void upd(){
sum=c[0]->sum+c[1]->sum+val;
mxl=max(c[0]->mxl,max(c[0]->sum+val+flt,c[0]->sum+val+c[1]->mxl));
mxr=max(c[1]->mxr,max(c[1]->sum+val+flt,c[1]->sum+val+c[0]->mxr));
ans=max(max(c[0]->mxr+c[1]->mxl,max(c[0]->mxr,c[1]->mxl)+flt),flt+slt)+val;
ans=max(ans,max(max(c[0]->ans,c[1]->ans),fmxa));
}
inline void rse(){swap(c[0],c[1]),swap(mxl,mxr),rev^=1;}
inline void pd(){if (rev) c[0]->rse(),c[1]->rse(),rev=0;}
void push(){if (!isr) fa->push(); pd();}
inline bool getson(){return fa->c[0]!=this;}
inline void rotate(){
Node *y=fa,*z=y->fa;
if (y->isr)
isr=1,y->isr=0;
else
z->c[y->getson()]=this;
int k=getson();
fa=z;
y->c[k]=c[k^1];
c[k^1]->fa=y;
c[k^1]=y;
y->fa=this;
sum=y->sum;
mxl=y->mxl,mxr=y->mxr;
ans=y->ans;
y->upd();
}
inline void splay(){
push();
while (!isr){
if (!fa->isr)
getson()!=fa->getson()?rotate():fa->rotate();
rotate();
}
}
inline Node *access(){
Node *x=this,*y=null;
for (;x!=null;y=x,x=x->fa){
x->splay();
y->isr=0;
if (y!=null){
x->lt.erase(y->mxl);
if (x->flt==y->mxl){
if (!x->lt.empty()){
x->flt=*x->lt.rbegin();
x->slt=(x->lt.size()>1?*(++x->lt.rbegin()):0);
}
else
x->flt=x->slt=0;
}
else if (x->slt==y->mxl)
x->slt=(x->lt.size()>1?*(++x->lt.rbegin()):0);
x->mxa.erase(y->ans);
if (x->fmxa==y->ans)
x->fmxa=(!x->mxa.empty()?*x->mxa.rbegin():0);
}
if (x->c[1]!=null){
x->c[1]->isr=1;
if (x->c[1]->mxl>x->flt)
x->slt=x->flt,x->flt=x->c[1]->mxl;
else if (x->c[1]->mxl>x->slt)
x->slt=x->c[1]->mxl;
x->lt.insert(x->c[1]->mxl);
if (x->c[1]->ans>x->fmxa)
x->fmxa=x->c[1]->ans;
x->mxa.insert(x->c[1]->ans);
}
x->c[1]=y;
x->upd();
}
return y;
}
inline void mroot(){access()->rse();}
inline void link(Node *x){
mroot();
splay();
x->mroot();
x->splay();
x->fa=this;
if (x->mxl>flt)
slt=flt,flt=x->mxl;
else if (x->mxl>slt)
slt=x->mxl;
lt.insert(x->mxl);
if (x->ans>fmxa)
fmxa=x->ans;
mxa.insert(x->ans);
upd();
}
inline void cut(Node *x){
mroot();
x->access();
splay();
c[1]->fa=null;
c[1]->isr=1;
c[1]=null;
upd();
}
} d[N];
void init(int x,int fa){
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa){
d[x].link(&d[ei->to]);
init(ei->to,x);
}
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
e[ne]={v,last[u]};
last[u]=e+ne++;
e[ne]={u,last[v]};
last[v]=e+ne++;
}
null=d;
null->fa=null->c[0]=null->c[1]=null;
null->ans=0;
for (int i=1;i<=n;++i){
d[i].fa=d[i].c[0]=d[i].c[1]=null;
d[i].isr=1;
int x;
scanf("%d",&x);
d[i].val=d[i].sum=d[i].mxl=d[i].mxr=d[i].ans=x;
d[i].flt=d[i].slt=d[i].fmxa=0;
}
init(1,0);
d[1].mroot(),d[1].splay();
printf("%d\n",d[1].ans);
while (m--){
char ch[2];
scanf("%s",ch);
if (*ch=='M'){
int x,y;
scanf("%d%d",&x,&y);
d[x].mroot();
d[x].splay();
d[x].val=y;
d[x].upd();
printf("%d\n",d[x].ans);
}
else{
int u,v,x,y;
scanf("%d%d%d%d",&u,&v,&x,&y);
d[u].cut(&d[v]);
d[x].link(&d[y]);
d[x].mroot();
d[x].splay();
printf("%d\n",d[x].ans);
}
}
return 0;
}
第二题
给你\(n\)个一次函数,你要选择总共\(p\)个一次函数,求最小的\(\sum_{i=1}^{n} k_i*(i-1)+b_i\)
题目有个\(K\),\(K\)为\(0\)或\(1\),如果为\(1\),意味着你可以将一个\(b_i\)变成\(0\)。
\(n\leq 100000\)
由于多数时间在刚第一题,所以我这题只是匆匆打了个暴力……
结果出了之后发现暴力都打挂了……
然而我看不出是哪里挂了……
【正解】
%%%GMH
首先,非常显然的是,如果你知道你要选那些一次函数,那就按照\(k_i\)给它们排序。
所以一开始就将所有一次函数按照\(k_i\)从大到小排一遍序。
考虑你一开始将\(n\)个都选择了,每次要减去一条一次函数,使得减去贡献最大。
如果删掉一条一次函数,记它的编号为\(x\),那么就要减去\(k_x(x-1)+b_x\)以及后面的\(k_i\)之和。
那我们对于所有的一次函数,计算出如果将其删掉,就会有多少贡献。
维护一下最大的贡献,每次找贡献最大的,进行修改。
可以随便推一下修改的式子是长什么样的,由于有个类似于一次函数的平移操作,所以就要搞个分块,然后块内维护一个凸包……
对于\(K=1\)的情况,可以计算完\(K=0\)的情况之后,分类讨论:将找到的\(p\)个中选择最大的\(b_i\),将其变成\(0\);或者是枚举不要\(p\)个中的某一个,然后通过数据结构求出要把哪个加进来贡献最大。
不会证明……
第三题
\(n\)个人,\(3\)种物品,然后有若干条关系:
如果某个人选择了什么物品,那么另一个人就要选择一个集合中的某个物品。
问最多能满足的关系是多少。
提交答案题。
不会……
Day10 12.17
第一题
给你一个长度为\(n\)的序列,其中\(a_i\)表示这个序列上有\(a_i\)的概率为这个位置是\(1\),否则是\(0\)。
形成的01序列会有若干段连续的\(1\),对于一个长度为\(k\)的序列,它会有\(b^k\)的贡献。
让你给出期望的贡献。用关于\(b\)的多项式来表达。
\(n\leq 50000\)
只想到了\(O(n^3)\)的DP……
而且长得一点都不像卷积……
【正解】
枚举长度,分别求出每个长度的期望出现次数。
枚举区间,那么这个区间全部选\(1\)的概率是中间的\(a_i\)的乘积以及两旁的\(1-a_i\)的乘积。
把这个式子写出来,你就会发现这个东西很像一个卷积。
然后直接套FFT就好。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 50010
#define MM 1048576
int M,Mb;
const double PI=acos(-1);
int n;
double a[MM],p[MM],q[MM];
double f[MM];
struct com{
double x,y;
com(double _x=0,double _y=0){x=_x,y=_y;}
};
inline com operator+(const com &a,const com &b){return (com){a.x+b.x,a.y+b.y};}
inline com operator-(const com &a,const com &b){return (com){a.x-b.x,a.y-b.y};}
inline com operator*(const com &a,const com &b){return (com){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
com A[MM],B[MM],C[MM];
int rev[MM];
inline void fft(com A[],int flag){
for (int i=0;i<M;++i)
if (i<rev[i])
swap(A[i],A[rev[i]]);
for (int i=1;i<M;i<<=1){
com wn(cos(flag*PI/i),sin(flag*PI/i));
for (int j=0;j<M;j+=i<<1){
com wnk(1,0);
for (int k=j;k<j+i;++k,wnk=wnk*wn){
com x=A[k],y=wnk*A[k+i];
A[k]=x+y;
A[k+i]=x-y;
}
}
}
if (flag==-1)
for (int i=0;i<M;++i)
A[i].x/=M;
}
void work(double a[],double b[],double c[]){
for (int i=1;i<M;++i)
rev[i]=rev[i>>1]>>1|(i&1)<<Mb-1;
/*for (int i=0;i<M;++i){
int tmp=0;
for (int j=0,k=i;j<20;++j,k>>=1)
tmp=(tmp<<1)+(k&1);
rev[i]=tmp;
}*/
for (int i=0;i<M;++i){
A[i]=(com){a[i],0};
B[i]=(com){b[i],0};
}
fft(A,1),fft(B,1);
for (int i=0;i<M;++i)
C[i]=A[i]*B[i];
fft(C,-1);
for (int i=0;i<M;++i)
c[i]=C[i].x;
}
int main(){
scanf("%d",&n);
for (int i=1;i<=n;++i)
scanf("%lf",&a[i]);
p[0]=1,q[0]=1;
p[1]=a[1],q[1]=1/a[1];
for (int i=2;i<=n;++i){
p[i]=p[i-1]*a[i];
q[i]=1/p[i];
}
for (int i=1;i<=n;++i)
p[i]*=1-a[i+1],q[i]*=1-a[i];
for (int i=0;i<=n && i<n-i;++i)
swap(q[i],q[n-i]);
p[0]=0,q[0]=0;
/*for (int i=1+n;i<=n+n;++i)
for (int j=i-n;j<=n;++j)
f[i]+=p[j]*q[i-j];*/
M=1,Mb=0;
for (;M<=n+n;M*=2,++Mb);
work(p,q,f);
printf("%.12lf\n",0);
for (int i=1+n;i<=n+n;++i)
printf("%.12lf\n",f[i]);
return 0;
}
第二题
给你若干个平面上的矩形,你要从第一个矩形的左下角出发,依次经过每一个矩形(到达边界或内部)。
问路程长度的最小值(曼哈顿距离)。
给出的都是矩形的中心位置,矩形的边心距为自变量,让你求这个东西的分段函数。
\(n\leq 2e5\)
比赛时看这题觉得很神仙,根本就没有想过……
【正解】
由于题目让你求的是曼哈顿距离,所以就可以将横坐标和纵坐标分开做。
于是二维问题被简化成了一维问题:从某个点出发,依次经过所有的线段,路程的最小值。
先假设边心距为无限小,这个时候每条线段都可以看成一个点。
那么依次经过这些点的路程就是答案。
我们可以将路径分成一段向左、一段向右、一段向左……这样若干个部分。
边心距扩大时,有些线段就会相交。
相交的时候,对于在同一个部分和在不同部分的情况分别处理一下……
用数据结构来简单维护就好了。
第三题
给你一个序列,支持翻转操作和询问操作。
询问某个区间中,将其中任意一个数改成任意值,能够得到的最小方差。
\(n \leq 1e5\)
一看就会正解了,只可惜题目没有开spj……
【正解】
看到这题就知道题目不会难到哪里去。
随便推了推式子,发现要改的数是序列中的最大值或最小值。
至于改成什么已经忘了,随意推一下就好……
维护最大值、最小值、和、平方和、大小就行。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
#define N 100010
#define ll long long
#define db __float128
int n;
int a[N];
struct Node *rt,*null;
struct Node{
Node *fa,*c[2];
bool rev;
int val,mn,mx,sum,siz;
ll sum2;
inline bool getson(){return fa->c[0]!=this;}
inline void upd(){
mn=min(min(c[0]->mn,c[1]->mn),val);
mx=max(max(c[0]->mx,c[1]->mx),val);
sum=c[0]->sum+c[1]->sum+val;
sum2=c[0]->sum2+c[1]->sum2+(ll)val*val;
siz=c[0]->siz+c[1]->siz+1;
}
inline void rse(){swap(c[0],c[1]),rev^=1;}
inline void pd(){if (rev) c[0]->rse(),c[1]->rse(),rev=0;}
inline void rotate(){
Node *y=fa,*z=y->fa;
if (z!=null)
z->c[y->getson()]=this;
int k=getson();
fa=z;
y->c[k]=c[k^1];
c[k^1]->fa=y;
c[k^1]=y;
y->fa=this;
mn=y->mn,mx=y->mx,sum=y->sum,siz=y->siz,sum2=y->sum2;
y->upd();
}
} d[N];
int cnt;
inline Node *newnode(){return &(d[++cnt]=(Node){null,null,null,0,0,INT_MAX,INT_MIN,0,0,0});}
Node *build(int l,int r){
if (l>r)
return null;
int mid=l+r>>1;
Node *rt=newnode();
rt->val=a[mid];
rt->c[0]=build(l,mid-1);
rt->c[1]=build(mid+1,r);
rt->c[0]->fa=rt->c[1]->fa=rt;
rt->upd();
return rt;
}
inline void splay(Node *x,Node *t){
while (x->fa!=t){
if (x->fa->fa!=t)
x->getson()!=x->fa->getson()?x->rotate():x->fa->rotate();
x->rotate();
}
if (t==null)
rt=x;
}
Node *kth(Node *t,int k){
t->pd();
if (k<=t->c[0]->siz)
return kth(t->c[0],k);
if (k>t->c[0]->siz+1)
return kth(t->c[1],k-t->c[0]->siz-1);
return t;
}
inline db sqr(db x){return x*x;}
inline db calc(db sum2,db sum,int n,db xp_){
db xp=(db)(sum-xp_)/(n-1);
sum2=sum2-sqr(xp_)+sqr(xp);
sum=sum-xp_+xp;
return sum2/n-sqr(sum/n);
}
int main(){
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int Q;
scanf("%d%d",&n,&Q);
for (int i=1;i<=n;++i)
scanf("%d",&a[i]);
null=d;
*null=(Node){null,null,null,0,0,INT_MAX,INT_MIN,0,0,0};
rt=build(0,n+1);
while (Q--){
char ch[2];
int x,y;
scanf("%s%d%d",ch,&x,&y);
if (ch[0]=='q'){
if (x==y){
printf("0.000000\n");
continue;
}
Node *L=kth(rt,x);
splay(L,null);
Node *R=kth(rt,y+2);
splay(R,L);
double ans1=calc(R->c[0]->sum2,R->c[0]->sum,y-x+1,R->c[0]->mn);
double ans2=calc(R->c[0]->sum2,R->c[0]->sum,y-x+1,R->c[0]->mx);
printf("%.6lf\n",min(ans1,ans2));
}
else{
Node *L=kth(rt,x);
splay(L,null);
Node *R=kth(rt,y+2);
splay(R,L);
R->c[0]->rse();
}
}
return 0;
}
Day11 12.18
第一题普及难度,忽略不讲(我一开始都不敢相信这道题目如此简单,然而就是AC了)。
第二题
给你若干个长度不一定相同的数列,每次你可以取其中两个数列\(A\)和\(B\),设\(|A|<|B|\)。
将\(A\)和\(B\)的长度为\(|A|\)的前缀交换。
问最后得到的最大数列的最大值是多少。
\(n\leq 5e5\)
数列总长度\(\leq 1e6\)
一开始转错了模型,搞出了个错误的东西。
后来模型一对,这道题就切了……
【正解】
有个很显然的结论是,某个值只能一直在数列上的固定位置。
接着,转对了模型,你会发现这变成了一个简单的问题:
将所有的长度搞出来,按照长度将所有数列分成若干段。将每段的数加起来变成个新数列。
然后对于新数列的每一列求最大值加起来就好……
using namespace std;
#include <cstdio>
#include <cstring>
#include <climits>
#include <algorithm>
#define N 500010
#define ALL 2000010
#define ll long long
int n;
int data[ALL],*point=data;
int *a[N];
bool cmp(int *x,int *y){return x[0]<y[0];}
ll mxs[ALL],f[ALL];
int main(){
scanf("%d",&n);
for (int i=1;i<=n;++i){
a[i]=++point;
scanf("%d",a[i]);
for (int j=1;j<=*a[i];++j){
++point;
scanf("%d",point);
}
}
sort(a+1,a+n+1,cmp);
a[n+1]=++point;
a[n+1][0]=-1;
memset(f,128,sizeof(ll)*(a[n][0]+1));
f[0]=0;
ll ans=LLONG_MIN;
for (int i=1,j=1;i<=n;++i)
if (a[i][0]!=a[i+1][0]){
memset(mxs,128,sizeof(ll)*(a[i][0]+1));
for (int t=j;t<=n;++t){
ll s=0;
for (int k=1,p=a[i][0];p;--p,++k){
s+=a[t][p];
mxs[k]=max(mxs[k],s);
}
}
for (int k=1;k<=a[i][0];++k)
f[a[i][0]]=max(f[a[i][0]],f[a[i][0]-k]+mxs[k]);
ans=max(ans,f[a[i][0]]);
j=i+1;
}/*
for (int i=1;i<=a[n][0];++i)
printf("%lld ",f[i]);
printf("\n");*/
printf("%lld\n",ans);
return 0;
}
第三题
有一个一维的棋盘,开始为空。有\(a,b,c\)三种棋子。
可以进行两种操作:
选择相邻的空格,用两种不同的棋子填它。
选择一个空格和与其相邻的棋子,用两种互不相同且不和这个原来的棋子相同的棋子填它。
\(n\leq 1e5\)
比赛时猜结论+乱搞搞出了WA20。
现在我都不知道正解是怎么样的……
DYP大佬hack掉了一堆AC的程序。
Day12 12.19
第一题
给你一棵树,每条边有一个权值(\(0\)或\(1\))。
你可以进行一些操作,使得某条路径上的所有边上的权值取反。
题目给定一些边必须变成\(0\),一些边不必变成\(0\)。
让你求最少的操作次数。
\(n\leq 1e5\)
简单树形DP……
然而我情况考虑不全,从而比赛时搞了很久都没有AC……
正解就不说了……
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 100010
int n;
char str[N];
int a[N],b[N];
struct EDGE{
int to;
EDGE *las;
} e[N*2];
int ne;
EDGE *last[N];
int f[N][2];
int ans;
void dp(int x,int fa){
int cnt1=0,_0=0,mn=1,sum=0;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa){
dp(ei->to,x);
if (b[ei-e>>1]==1){
sum+=f[ei->to][1];
cnt1++;
}
else if (b[ei-e>>1]==-1){
sum+=f[ei->to][0];
if (f[ei->to][1]-f[ei->to][0]==0)
_0++,mn=0;
}
else
ans+=f[ei->to][0];
}
if (cnt1<_0){
f[x][0]=sum-cnt1-(_0-cnt1>>1);
f[x][1]=sum-cnt1-(_0-cnt1-1>>1);
}
else{
f[x][0]=sum-_0-(cnt1-_0>>1);
if (cnt1>_0)
f[x][1]=sum-_0-(cnt1-_0-1>>1);
else
f[x][1]=sum-_0+1;
}
f[x][1]=min(f[x][1],f[x][0]+1);
assert(f[x][0]>=0 && f[x][1]>=1);
assert(f[x][0]<=f[x][1]);
}
int main(){
// freopen("in.txt","r",stdin);
scanf("%d%s",&n,str);
for (int i=0;i<n-1;++i)
a[i]=str[i]-'0';
scanf("%s",str);
for (int i=0;i<n-1;++i){
if (str[i]=='1')
b[i]=a[i];
else
b[i]=-1;
}
for (int i=0;i<n-1;++i){
int u,v;
scanf("%d%d",&u,&v);
e[ne]=(EDGE){v,last[u]};
last[u]=e+ne++;
e[ne]=(EDGE){u,last[v]};
last[v]=e+ne++;
}
dp(1,0);
ans+=f[1][0];
printf("%d\n",ans);
return 0;
}
第二题
给你若干个数,你要将它们划分成两个非空子集,使得\(and\)值相等。
求方案数.
\(n \leq 50\)
\(a_i < 2^20\)
比赛时想偏了……完全没有往容斥方面想……
【正解】
就是容斥。
枚举哪些位置不合法。
不合法是什么?就是这个位置上为\(0\)的数都在同一个集合里。
用并查集来搞,一个连通块表示这个连通块里的所有东西都在同一个集合。
计算方案数就通过连通块个数来计算。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 60
#define ll long long
int n,a[N],asum;
int m,lis[N];
int cho[N];
int dsu[N][N];
int getfa(int f,int x){return dsu[f][x]==x?x:dsu[f][x]=getfa(f,dsu[f][x]);}
ll ans;
void dfs(int x,int num){
if (x>m){
int cnt=0;
for (int i=1;i<=n;++i)
if (getfa(m,i)==i)
cnt++;
ans+=((1ll<<cnt)-2)*(num&1?-1:1);
return;
}
for (int i=1;i<=n;++i)
dsu[x][i]=dsu[x-1][i];
cho[x]=0;
dfs(x+1,num);
int z=0,i;
for (i=1;i<=n;++i)
if ((a[i]>>lis[x]&1)==0){
z=getfa(x,i);
break;
}
for (++i;i<=n;++i)
if ((a[i]>>lis[x]&1)==0)
dsu[x][getfa(x,i)]=z;
cho[x]=1;
dfs(x+1,num+1);
}
int main(){
//freopen("in.txt","r",stdin);
scanf("%d",&n);
asum=(1<<20)-1;
for (int i=1;i<=n;++i)
scanf("%d",&a[i]),asum&=a[i];
for (int i=0;i<20;++i)
if ((asum>>i&1)==0)
lis[++m]=i;
for (int i=1;i<=n;++i)
dsu[0][i]=i;
dfs(1,0);
printf("%lld\n",ans);
return 0;
}
第三题
给你一个平面,平面上很多圆。
问圆的并的面积。
圆心都在整点上。
圆的半径是题目给出两个数中的其中一个。范围分别在\((0.1,1)\)和\((1,1.4)\)
比赛时看着觉得是个计算几何的神仙题,所以没有打。
\(T\leq 200\)
\(N\leq 2000\)
【正解】
这其实就是一道模板题。
具体可以上网查。
套模板时间看起来似乎过不去,但是,如果注意到圆的半径的范围。
就会发现对于某个圆来说,和它相交的圆并不多。
找相交的时候就找圆心附近的点就好了,用个哈希之类的搞一搞。
Day13 12.20
学军游记已经结束了。
一大早去北京。
学军再见,杭州再见。