2023/2/1 考试总结
T1.P3195 [HNOI2008]玩具装箱
-
斜率优化 \(\mathtt{DP}\) 板题;
虽然这是板题但签到题就是紫的是否有些过分? -
\(f_i\) 表示把 \(i\) 及以前的都打包的最小花费。
朴素 \(DP\) 式子:\(f_i=\min\limits_{j=1}^{i-1}\{f_i,f_j+(sum_i-sum_j+i-j-L)^2\}\),\(sum\) 是长度的前缀和。这里为了方便处理,先把所有长度以及 \(L+1\),这样后面的平方里面就可以化简为 \(sum_i-sum_j-L\)。 -
转化:\(f_i=f_j+sum_i^2+sum_j^2+L^2-2sum_i sum_j-2sum_i L+2sum_j L\),
把含 \(i\) 项视作常数,然后与 \(k\) 相减得 \(f_j-f_k+sum_j^2-sum_k^2-2sum_i sum_j+2sum_i sum_k+2sum_j L-2sum_k L=0\)
于是设 \(y_i=f_i+sum_i^2\),\(x_i=sum_i\),
于是斜率 \(=\frac{y_j-y_k}{x_j-x_k}\),需要与 \(2sum_i-2L\) 比较。
AC code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=5e4+10;
int n,L;
ll c[N];
ll f[N];
int q[N],l,r;
#define y(i) (f[i]+c[i]*c[i])
double g(int a,int b){
return (double)(y(a)-y(b))/(double)(c[a]-c[b]);
}
int main(){
n=read(),L=read()+1;
for(int i=1;i<=n;++i)
c[i]=read()+1;
for(int i=1;i<=n;++i)
c[i]+=c[i-1];
l=r=1;
for(int i=1;i<=n;++i){
while(l<r && g(q[l],q[l+1])<0ll+1ll*2*c[i]-1ll*2*L)
++l;
f[i]=f[q[l]]+1ll*(0ll+L-c[i]+c[q[l]])*(0ll+L-c[i]+c[q[l]]);
// cerr<<i<<" "<<q[l]<<endl;
while(l<r && g(q[r-1],q[r])>=g(q[r],i))
--r;
q[++r]=i;
}
printf("%lld",f[n]);
return 0;
}
/*
5 4
3
4
2
1
4
*/
T2.P7118 Galgame
-
\(Catalan\) 数+树形 \(\mathtt{DP}\);
-
很容易想到,只要整棵树的节点数少于当前这棵树,那么不管它是什么形状,它都不会更有趣。
因此,树形 \(\mathtt{DP}\) 时只需要考虑节点数相同的情况。 -
设 \(f_i\) 表示,在 \(i\) 节点及其子树上不更有趣的方案数。
由于我们知道,\(Cat_n\) 可以表示一棵 \(n\) 个节点的二叉树的不同结构数,所以可以先预处理出一个 \(Catalan\) 数列(我使用了公式 \(Cat_n=\frac{4n-2}{n+1}Cat_{n-1}\),含有除法,所以先写了一个线性求逆元)
然后对于具体的 \(i\),由于是先判断左子树再看右子树,所以分为以下几种情况:- \(i\) 的左子树和原树上对应节点的大小相同,区别出现在再下面的地方(即 \(f_{ls_i}\));
- \(i\) 的左子树比原树上对应节点小,区别出现在这里;
- \(i\) 的左子树与原树一样,区别出现在右子树(即 \(f_{rs_i}\));
对于第一种情况,在左子树中发现区别后就不会再看右子树,所以右子树可以是任何形状,\(f_i+=f_{ls_i}Cat_{size_{rs_i}}\);
对于第二种情况,只需要保持 \(i\) 及其子树大小恒定,\(f_i+=\sum\limits_{j=0}^{size_{ls_i}-1}Cat_i Cat_{size_i-j}\);
对于第三种情况,\(f_i+=f_{rs_i}\)
-
优化:第二种情况中,有一种可能是左子树大小比右子树大,又有 \(Cat_n=\sum\limits_{i=0}^{n-1}Cat_i Cat_{n-i-1}\),所以可以用这个性质进行优化,当左子树比右子树大时,就用右子树来算;
时间复杂度从 \(\mathtt{O(n^2)}\) 到 \(\mathtt{O(n\log n)}\);
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
#define ll long long
const int N=1e6+10;
const int mod=998244353;
int n;
int ls[N],rs[N];
ll f[N],siz[N];
ll cat[N],sumc[N];
ll inv[N];
void dfs(int p){
if(!p) return ;
dfs(ls[p]);
dfs(rs[p]);
siz[p]=siz[rs[p]]+siz[ls[p]];
if(ls[p]){
if(siz[ls[p]]<=siz[rs[p]])
for(int i=0;i<siz[ls[p]];++i)
f[p]+=cat[i]*cat[siz[p]-i]%mod;
else{
(f[p]+=cat[siz[p]+1])%=mod;
for(int i=siz[p];i>=siz[ls[p]];--i)
f[p]+=mod-cat[i]*cat[siz[p]-i]%mod;
}
(f[p]+=f[ls[p]]*cat[siz[rs[p]]]%mod)%=mod;
}
(f[p]+=f[rs[p]])%=mod;
++siz[p];
return ;
}
int main(){
n=read();
for(int i=1;i<=n;++i)
ls[i]=read(),rs[i]=read();
inv[1]=1;
for(int i=2;i<=n+1;++i)
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
cat[0]=cat[1]=1;
for(int i=2;i<=n;++i)
cat[i]=1ll*cat[i-1]*(4*i-2)%mod*inv[i+1]%mod;
sumc[0]=1;
for(int i=1;i<=n;++i)
sumc[i]=(sumc[i-1]+cat[i])%mod;
dfs(1);
(f[1]+=sumc[n-1]-1)%=mod;
printf("%lld",f[1]);
return 0;
}
/*
9
2 3
4 5
0 0
0 0
6 7
0 0
8 9
0 0
0 0
*/
T3.P1399 [NOI2013] 快餐店
-
基环树;
-
感觉我的树和图的知识都需要重修一次,难道这就是当年翘课去物竞的后遗症吗,恐怖如斯 -
一个基环树+spfa 嫖了 50pts 这是可以说的吗 -
如果原图是一棵树,那么问题很好解决:找到直径,然后 \(/2\);
因此,问题可以转化为:对一棵基环树找到最小直径。
然后分两种情况讨论:- 该直径不经过环。
找到环,枚举环上每个点,然后对点不包括环的子树进行遍历,找直径即可。 - 该直径经过环。
一种朴素的办法是枚举环上的断边,然后暴力求解,但时间复杂度 \(\mathtt{O(n^2)}\),过不了。
我们先找到环,然后把环上的点重新从 \(1\sim k\) 编一下号。
设置 \(4\) 个数组(代码中的 \(cnt_{0\sim 3}\))。
- 该直径不经过环。
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=1e5+10;
int n;
int head[N],ver[N<<1],nxt[N<<1],tot=1;
double edge[N<<1];
void add(int x,int y,double z){
ver[++tot]=y,edge[tot]=z;
nxt[tot]=head[x],head[x]=tot;
return ;
}
bool vis[N],is[N];
int pt[N],k=0;
int f[N],w[N],c[N];
bool find(int p,int fa){
vis[p]=1;
for(int i=head[p];i;i=nxt[i]){
if(ver[i]==fa) continue;
w[ver[i]]=edge[i];
f[ver[i]]=p;
if(!vis[ver[i]]){
if(find(ver[i],p))
return 1;
}
else{
for(int i=p;i;){
pt[++k]=i;
is[i]=1;
c[k]=w[i];
i=f[i];
if(i==p) break;
}
return 1;
}
}
return 0;
}
double ans[2],dep[N];
void dfs(int p,int fa){
for(int i=head[p];i;i=nxt[i]){
if(ver[i]==fa || is[ver[i]]) continue;
dfs(ver[i],p);
ans[0]=max(ans[0],(double)dep[p]+dep[ver[i]]+edge[i]);
dep[p]=max(dep[p],dep[ver[i]]+edge[i]);
}
return ;
}
double cnt[5][N];
int main(){
// freopen("2.in","r",stdin);
n=read();
int a,b;
double l;
for(int i=1;i<=n;++i){
a=read(),b=read(),l=read();
add(a,b,l);
add(b,a,l);
}
find(1,0);
for(int i=1;i<=k;++i)
dfs(pt[i],0);
double sum=0,mx=0;
for(int i=1;i<=k;++i){
sum+=c[i-1];
cnt[0][i]=max(cnt[0][i-1],sum+dep[pt[i]]);
cnt[1][i]=max(cnt[1][i-1],sum+mx+dep[pt[i]]);
mx=max(mx,dep[pt[i]]-sum);
}
sum=mx=0;
int cw=c[k];
c[k]=0;
for(int i=k;i;--i){
sum+=c[i];
cnt[2][i]=max(cnt[2][i+1],sum+dep[pt[i]]);
cnt[3][i]=max(cnt[3][i+1],sum+mx+dep[pt[i]]);
mx=max(mx,dep[pt[i]]-sum);
}
double ans1=1e18;
for(int i=1;i<=k;++i){
ans[1]=max(cnt[1][i],cnt[3][i+1]);
ans[1]=max(ans[1],cnt[0][i]+cnt[2][i+1]+cw);
ans1=min(ans1,ans[1]);
}
ans1=min(ans1,cnt[1][k]);
printf("%.1f",max(ans1,ans[0])/2.0);
return 0;
}
T4.P5607 [Ynoi2013] 无力回天 NOI2017
-
好名字 -
线段树+线性基;
-
区间查询,很显然的树状数据结构。可以维护线性基,然后每次暴力合并。问题是如何维护区间异或操作。
线性基难以支持修改操作,所以我们考虑把区间修改转换成单点修改,对原数列进行差分。
易证,差分后的序列和原序列的线性基等效。
因此,把差分后的序列塞进线段树套线性基里,然后另开一个树状数组支持单点修改和区间查询。然后再每次查询时把序列第一个前缀起来放进线性基,就可以正常查询了。 -
不过本题有点卡常,线性基的数组但凡再开大都要 \(T\)(也可能我的写法有些地方不太优秀吧);
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
#define re register
const int N=5e4+10;
int n,m;
int a[N];
int c[N];
struct memr{
int l,r;
int q[30];
}tr[N<<3];
//树状数组
void ax(int x,int v){
for(;x<=n;x+=x&-x)
c[x]^=v;
return ;
}
int asx(int x){
int cnt=0;
for(;x;x-=x&-x)
cnt^=c[x];
return cnt;
}
//
void check(int *x,int y){
for(re int i=29;i>-1;--i){
if(!((y>>i))&1) continue;
if(!x[i]){
x[i]=y;
break;
}
y^=x[i];
}
return ;
}
void merge(int *x,int *y){
for(re int i=29;i>-1;--i){
if(!x[i]) continue;
check(y,x[i]);
}
return ;
}
void pushup(int p){
for(re int i=0;i<30;++i)
tr[p].q[i]=tr[p<<1].q[i];
merge(tr[p<<1|1].q,tr[p].q);
return ;
}
void build(int p,int l,int r){
tr[p].l=l,tr[p].r=r;
if(l==r){
check(tr[p].q,a[l]);
return ;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
return ;
}
void change(int p,int x,int v){
if(x<=tr[p].l && tr[p].r<=x){
memset(tr[p].q,0,sizeof(tr[p].q));
check(tr[p].q,a[x]);
return ;
}
int mid=(tr[p].l+tr[p].r)>>1;
if(x<=mid) change(p<<1,x,v);
else change(p<<1|1,x,v);
pushup(p);
return ;
}
void ask(int p,int l,int r,int *x){
if(l<=tr[p].l && tr[p].r<=r){
merge(tr[p].q,x);
return ;
}
int mid=(tr[p].l+tr[p].r)>>1;
if(l<=mid) ask(p<<1,l,r,x);
if(mid<r) ask(p<<1|1,l,r,x);
pushup(p);
return ;
}
int g(int *x,int y){
for(re int i=29;i>-1;--i)
if((y^x[i])>y)
y^=x[i];
return y;
}
int ac[30];
int main(){
n=read(),m=read();
for(re int i=1;i<=n;++i)
a[i]=read();
for(re int i=n;i;--i){
a[i]^=a[i-1];
ax(i,a[i]);
}
build(1,1,n+1);
int opt,l,r,v;
while(m--){
opt=read(),l=read(),r=read(),v=read();
if(opt==1){
ax(l,v);
ax(r+1,v);
a[l]^=v,a[r+1]^=v;
change(1,l,v);
change(1,r+1,v);
}
else{
int t=asx(l);
if(l==r){
printf("%d\n",max(v,v^t));
continue;
}
memset(ac,0,sizeof(ac));
ask(1,l+1,r,ac);
check(ac,t);
printf("%d\n",g(ac,v));
}
}
return 0;
}