联合省选2021A卷 补题记录
明年再见罢,可能没有明年了
D1T1 卡牌游戏
心态从这里开始炸的,口胡的结论不加以证明,也没举反例就随便写是我的老毛病了,现在也算是吃到了恶果。
正解应该是枚举 最小(或最大值),再去二分(或者用双指针)去找到另一头的最值,记录一下最小的极差。
刚进考场第一眼写的就是这个,可惜排序顺序假了,我并没有按照 \(a\) 和 \(b\) 的最小值排序,只按了 \(a\) 排序,调了一个小时也没有过样例,直接放弃了这种写法。
转而开始口胡结论:答案关于最小/最大值是一个凸函数。随便举几个例子都能hack这个结论,但是场上犯病了,没去多想,觉得很有道理就直接去码了,再加上巨水无比的样例...
用了基排,总复杂度是 \(O(n)\) 的。
Code
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define ll long long
#define ui unsigned int
il ll read(){
bool f=true;ll x=0;
register char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=false;ch=getchar();}
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(f) return x;
return ~(--x);
}
il void write(const ll &x){if(x>9) write(x/10);putchar(x%10+'0');}
il void print(const ll &x) {x<0?putchar('-'),write(~(x-1)):write(x);putchar('\n');}
il ll max(const ll &a,const ll &b){return a>b?a:b;}
il ll min(const ll &a,const ll &b){return a<b?a:b;}
ll n,m,ans;
const int MAXN=2e6+7;
struct node
{
int v,id,w;
}a[MAXN];
int del[MAXN];
namespace Sort{
int cnt1[256],cnt2[256],cnt3[256],cnt4[256];
node temp[MAXN];
void mysort(node a[],int l,int r){
ri i;
node *p;
for (i=l,p=a+l;i <= r;++ i,++p){
++ cnt1[p->v & 255];
++ cnt2[p->v >> 8 & 255];
++ cnt3[p->v >> 16 & 255];
++ cnt4[p->v >> 24 & 255];
}
for (i = 1;i <= 255;++ i){
cnt1[i] += cnt1[i - 1];
cnt2[i] += cnt2[i - 1];
cnt3[i] += cnt3[i - 1];
cnt4[i] += cnt4[i - 1];
}
for (i = r,p=&a[r];i >= l;-- i,--p) temp[cnt1[p->v & 255] --] = *p;
for (i = r,p=&temp[r];i >= l;-- i,--p) a[cnt2[p->v >> 8 & 255] --] = *p;
for (i = r,p=&a[r];i >= l;-- i,--p) temp[cnt3[p->v >> 16 & 255] --] = *p;
for (i = r,p=&temp[r];i >= l;-- i,--p) a[cnt4[p->v >> 24 & 255] --] = *p;
}
}
int main(){
// freopen("card/card3.in","r",stdin);
// freopen("card/card3.out","w",stdout);
n=read(),m=read();
for(ri i=1;i<=n;++i) a[i].v=read(),a[i].id=i,a[i].w=1;
for(ri i=1;i<=n;++i) a[n+i].v=read(),a[n+i].id=i;
Sort::mysort(a,1,n<<1);
ll l=0,r=2*n+1,use=0;
while(!del[a[l+1].id]&&use+a[l+1].w<=m){
l++;
del[a[l].id]=1;
use+=a[l].w;
}
while(!del[a[r-1].id]&&use+a[r-1].w<=m){
r--;
del[a[r].id]=1;
use+=a[r].w;
}
ans=1e18;
while(~l){
ans=min(a[r-1].v-a[l+1].v,ans);
use-=a[l].w;
del[a[l].id]=0;
while(!del[a[r-1].id]&&use+a[r-1].w<=m){
r--;
del[a[r].id]=1;
use+=a[r].w;
}
l--;
}
print(ans);
return 0;
}
D1T2 矩阵
考场上一眼看出来是差分约束,但是没建出来模型,也忘了怎么敲差分约束,甚至连 \(m=2\) 的点都写挂了构造题不发checker是真的牛批。
首先,先考虑如果没有 \([0,10^6]\) 的限制,可以直接钦定第一行和第一列(一般来说钦定为多少都可以)。
然后可以发现已经可以把整个矩形给推出来了。
然后有这样一个性质,对于某一行来说,将这一行中的元素 \(+1,-1,+1,-1 \dots\) 这样交替加减是不会影响 \(b\) 数组的。
于是,设数组 \(r_j\) 表示在第 \(j\) 列的操作数,\(c_i\) 表示在第 \(i\) 行的操作数。
大概可以列出来这样一个矩阵:
分奇偶讨论跑差分约束 队列实现SPFA会被卡,建议用栈实现
Code
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define ll long long
#define ui unsigned int
il ll read(){
bool f=true;ll x=0;
register char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=false;ch=getchar();}
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(f) return x;
return ~(--x);
}
il void write(const ll &x){if(x>9) write(x/10);putchar(x%10+'0');}
il void print(const ll &x) {x<0?putchar('-'),write(~(x-1)):write(x);putchar('\n');}
il ll max(const ll &a,const ll &b){return a>b?a:b;}
il ll min(const ll &a,const ll &b){return a<b?a:b;}
const int MAXN=305+7;
ll a[MAXN][MAXN],b[MAXN][MAXN],n,m;
ll vis[MAXN<<1],dis[MAXN<<1],in[MAXN<<1];
il int id(int x,int y){return x*(m-1)+y;}
struct edge
{
ll u,w;
};
vector<edge> g[MAXN<<1];
bool SPFA(){
queue<int> q;
for(ri i=1;i<=n+m;++i) vis[i]=dis[i]=0,q.push(i),in[i]=1;
while(!q.empty()){
int s=q.front();q.pop();
in[s]=0;
for(ri i=0;i<g[s].size();++i){
edge now=g[s][i];
if(dis[s]+now.w<dis[now.u]){
dis[now.u]=dis[s]+now.w;
if(!in[s]){
q.push(now.u);
if(++vis[now.u]>n+m){
return 0;
}
in[now.u]=1;
}
}
}
}
return 1;
}
int main(){
for(ri t=read();t;--t){
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
n=read(),m=read();
for(ri i=1;i<=n+m;++i) g[i].clear(),vis[i]=dis[i]=0;
for(ri i=1;i<n;++i)
for(ri j=1;j<m;++j)
b[i][j]=read();
for(ri i=1;i<n;++i){
for(ri j=1;j<m;++j){
a[i+1][j+1]=b[i][j]-a[i][j]-a[i][j+1]-a[i+1][j];
}
}
for(ri i=1;i<=n;++i){
for(ri j=1;j<=m;++j){
int l,r;
if((i+j)&1) r=j,l=m+i;
else r=m+i,l=j;
g[l].push_back((edge){r,a[i][j]});
g[r].push_back((edge){l,-a[i][j]+(ll)1e6});
}
}
if(SPFA()){
puts("YES");
for(ri i=1;i<=n;++i){
for(ri j=1;j<=m;++j){
if((i+j)&1) a[i][j]-=dis[j]-dis[m+i];
else a[i][j]+=dis[j]-dis[m+i];
// if(j&1) a[i][j]-=dis[m+i];
// else a[i][j]+=dis[m+i];
printf("%lld ",a[i][j]);
}
puts("");
}
}
else puts("NO");
}
return 0;
}
D1T3 图函数
开这题的时候已经只剩下半个多小时了,只敲了个暴力上去,顺便得到了一个性质:
如果能从 \(v\) 能对 \(u\) 造成贡献,那么途中不能有比 \(v\) 小的点。
这个性质应该挺显然的,如果途中有比 \(v\) 小的点,那么它一定在之前已经被删除了。
实际上,有了这个性质之后,已经可以得到一个 \(O(n^3+m)\) 的做法了 %zjjws。
把删边改成从 \(m\) 到 \(1\) 依次加边,记录从 \(u\) 到 \(v\) 的路径最早是在什么时候出现的,这个可以用 \(Floyd\) \(O(n^3)\)实现,然后做一个差分。
所以总复杂度 \(O(n^3+m)\)。
看上去似乎无法通过,实际上这个 \(Floyd\) 的常数是 \(\frac{1}{2}\) ,并且只有 \(\min \max\) 两种操作,手写函数之后加一点寻址优化还是能比较轻松的卡过去的指1s的时限要跑994ms。
Code
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define ll long long
#define ui unsigned int
il ll read(){
bool f=true;ll x=0;
register char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=false;ch=getchar();}
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(f) return x;
return ~(--x);
}
il void write(const ll &x){if(x>9) write(x/10);putchar(x%10+'0');}
il void print(const ll &x) {x<0?putchar('-'),write(~(x-1)):write(x);putchar('\n');}
il ll max(const ll &a,const ll &b){return a>b?a:b;}
il ll min(const ll &a,const ll &b){return a<b?a:b;}
const int MAXN=1e3+7;
int f[MAXN][MAXN];
ll ans;
int n,m;
const int MAXM=2e5+7;
ll h[MAXM];
int main(){
//freopen("2.in","r",stdin);
//freopen("1.out","w",stdout);
n=read(),m=read();
memset(f,0x3f,sizeof(f));
for(ri i=1;i<=n;++i) f[i][i]=0;
for(ri i=1;i<=m;++i){
int u=read(),v=read();
f[u][v]=m-i+1;
}
for(ri i=n;i;--i){
for(ri j=1;j<=n;++j){
if(f[j][i]>m) continue;
if(j<i){
if(j&1){
for(ri k=1;k<=n;++k)
f[j][k]=min(f[j][k],max(f[j][i],f[i][k]));
}
else{
for(ri k=n;k;--k)
f[j][k]=min(f[j][k],max(f[j][i],f[i][k]));
}
}
else if(j>i){
if(j&1){
for(ri k=1;k<i;++k)
f[j][k]=min(f[j][k],max(f[j][i],f[i][k]));
}
else{
for(ri k=i-1;k;--k)
f[j][k]=min(f[j][k],max(f[j][i],f[i][k]));
}
}
}
}
for(ri i=1;i<=n;++i){
for(ri j=1;j<=i;++j){
if(f[i][j]<=m&&f[j][i]<=m) h[max(f[i][j],f[j][i])]++;
}
}
for(ri i=1;i<=m;++i) h[i]+=h[i-1];
for(ri i=m;~i;--i) write(h[i]),putchar(' ');
return 0;
}
D2T1 宝石
算是两天里唯一一道写出来的题目了
看到题目一眼树剖,敲完树剖才发现这个东西并不好维护。
不知道为啥,第一反应是分块似乎是Dpair讲过类似的题目。
如果是在序列上,记录一个数组 \(nxt[i][j]\) ,表示在块 \(i\) 的头上以收集器中第 \(j\) 个颜色出发,离开块尾之后的最多能到收集器中的第几个颜色。(为方便,后面的颜色就直接指的是在收集器中对应的位置)
考虑如何维护这个东西。
等价于维护一个块内最长的形如 \(x,x+1\dots x+l\) 序列的最长长度是多少。
不难发现可以把这个序列拆成 \(x\) 和 \(x+1,x+2\dots x+l\)。
因此考虑倒过来做,从后往前枚举当前的下标,如果当前的颜色是 \(c_i\),当前以 \(c_i\) 开头的最长序列是 \(S\) 那么就看 \(c_{i-1}+S\) 是不是比当前 \(c_{i-1}\) 开头的序列更长。
同时,因为还有可能是反过来走的,所以还要记录一下反过来的数组 \(pre[i][j]\)。
具体的实现可以看最下面的代码中的init函数。
树剖实际上是把树上问题转换成序列问题,在每一条重链上的询问都可以看作是一个三元组 \((l,r,c)\) ,表示从 \(l\) 到 \(r\) 出发,初始颜色是 \(c\) 。
跳到 \(lca\) 处最多经过 \(\log n\) 条重链,所以有一次询问的复杂度 \(
\left \{
\begin{aligned}
& O(T)=\sum^{t}_{i=1}\sqrt{a_i}\\
& \sum^{t}_{i=1}a_i \leq n\\
& t\leq \log n\\
\end{aligned}
\right.
\)
不难得到上面的最劣情况复杂度为\(
t\sqrt{\frac{n}{t}}=\sqrt{nt}
\)
所以这样子的总复杂度是\(O(n\sqrt{n}+q\sqrt{n\log n})\),而且几乎跑不满,在随机数据下跑得还挺快,极限数据大概跑\(0.3~0.5s\)。
实际上,这个还可以继续优化。
考虑每一条重链,在链顶和链尾也像之前分块这样维护数组 \(nxt\) 和 \(pre\) ,时空复杂度均为 \(O(n\log n)\)(这里视n,m同阶)。
这样,每次需要在链上暴力分块的只剩下了开头和结尾两根,剩下的都可以 \(O(1)\) 转移,总复杂度优化到了 \(O(n\log n+q(\sqrt{n}+\log n))\)
不过考场上觉得第二种太麻烦了,而且第一个看起来能过,再加上时间不够,就没去优化。
下面这份代码是考场上写的,理论复杂度是有些问题的,正确的分块预处理方式应该是对于每一条重链都以该重链的长度的根号为块长分块才能保证单次询问\(O(\sqrt {n \log n})\)。
图一个方便,再加上实际运行效率差不多,甚至更优秀,就直接以512为块长了。
Code
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ll long long
#define ri register int
il ll read(){
ll x=0;
char ch=getchar(),f=1;
for(;ch<'0'||ch>'9';ch=getchar())
if(ch=='-') f=0;
for(;ch>='0'&&ch<='9';ch=getchar())
x=x*10+ch-'0';
if(f) return x;
return -x;
}
il void write(ll x){
if(x>9) write(x/10);
putchar(x%10+'0');
}
il ll max(ll x,ll y){return x>y?x:y;}
il ll min(ll x,ll y){return x<y?x:y;}
il void print(ll x){
if(x<0) putchar('-'),x=-x;
write(x);
putchar('\n');
}
/*
for pai
*/
int n,m,c;
const int MAXN=2e5+7;
const int MAXM=1e5+7;
int p[MAXN],w[MAXN],id[MAXN];//收集器中的颜色,当前点的颜色,颜色在收集器中对应的下标
vector<int> g[MAXN];
int fa[MAXN],top[MAXN],dfn[MAXN],siz[MAXN],dep[MAXN],son[MAXN],idfn[MAXN],dson[MAXN],ddep[MAXN];
#define B 512
void dfs1(int u,int f,int deep){
fa[u]=f;
ddep[u]=dep[u]=deep;
siz[u]=1;
int mx=-1;
for(ri i=0;i<g[u].size();++i){
int v=g[u][i];
if(v==f) continue;
dfs1(v,u,deep+1);
siz[u]+=siz[v];
if(siz[v]>mx){
son[u]=v;
mx=siz[v];
}
}
}
int cnt;
void dfs2(int u,int f,int topf){
dson[topf]=u;
dfn[u]=++cnt;
idfn[cnt]=u;
top[u]=topf;
if(son[u]) dfs2(son[u],u,topf);
for(ri i=0;i<g[u].size();++i){
int v=g[u][i];
if(v==son[u]||v==f) continue;
dfs2(v,u,v);
}
}
int bpre[B][MAXM],bnxt[B][MAXM];
int solve_1(int l,int r,int now,int flag){
//printf("%d %d %d %d\n",l,r,flag,now);
if(flag){
for(ri i=l;i<=r;++i){
int u=idfn[i];
if(now<c&&p[now+1]==w[u]){
++now;
}
}
}
else{
for(ri i=r;i>=l;--i){
int u=idfn[i];
//print(u);
if(now<c&&p[now+1]==w[u]) ++now;
}
}
//print(now);
return now;
}
int solve_2(int l,int r,int now,int flag){
if(l/B==r/B) return solve_1(l,r,now,flag);
if(flag){
//puts("fuck");
now=solve_1(l,l/B*B+B-1,now,flag);
for(ri i=l/B+1;i<r/B;++i) now=bnxt[i][now];
now=solve_1(r/B*B,r,now,flag);
}
else{
//puts("fuck");
now=solve_1(r/B*B,r,now,flag);
for(ri i=r/B-1;i>l/B;--i) now=bpre[i][now];
now=solve_1(l,l/B*B+B-1,now,flag);
}
return now;
}
int LCA(int u,int v){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
return v;
}
int vec[MAXN],head;
int solve_3(int u,int v){
int now=0;
head=0;
int lca=LCA(u,v);
while(top[u]!=top[lca]){
vec[++head]=u;
u=fa[top[u]];
}
for(ri i=1;i<=head;++i){
now=solve_2(dfn[top[vec[i]]],dfn[vec[i]],now,0);
}
now=solve_2(dfn[lca],dfn[u],now,0);
head=0;
while(top[v]!=top[lca]){
vec[++head]=v;
v=fa[top[v]];
}
now=solve_2(dfn[lca],dfn[v],now,1);
for(ri i=head;i;--i){
now=solve_2(dfn[top[vec[i]]],dfn[vec[i]],now,1);
}
return now;
}
void init(){
for(ri i=0;i<=n/B;++i){
int l=i*B,r=min(n,l+B-1);
for(ri j=0;j<=c;++j) bpre[i][j]=j;
for(ri j=l;j<=r;++j){
int v=w[idfn[j]];
if(!id[v]) continue;
bpre[i][id[v]-1]=max(bpre[i][id[v]-1],bpre[i][id[v]]);
}
for(ri j=0;j<=c;++j) bnxt[i][j]=j;
for(ri j=r;j>=l;--j){
int v=w[idfn[j]];
if(!id[v]) continue;
bnxt[i][id[v]-1]=max(bnxt[i][id[v]-1],bnxt[i][id[v]]);
}
}
}
int main(){
//freopen("gem.in","r",stdin);
//freopen("gem.out","w",stdout);
n=read(),m=read(),c=read();
for(ri i=1;i<=c;++i) p[i]=read(),id[p[i]]=i;
for(ri i=1;i<=n;++i) w[i]=read();
for(ri i=1;i<n;++i){
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
dfs1(1,0,1);
dfs2(1,0,1);
init();
for(ri t=read();t;--t){
int u=read(),v=read();
print(solve_3(u,v));
}
}
D2T2 滚榜
看错题了,一开始统计的是 \(b\) 的方案数,直接敲了一个暴力,连样例都过不了,直接放弃了。
考虑暴力写法,首先是枚举最后的排列,再用一个贪心去检查是否合法。
具体地说,就是用尽量少的 \(b\) 来实现当前这个排列的要求。
不难发现刷榜顺序和最终的排名是反过来的,所以直接暴力模拟刷榜过程。
设上一次榜首是 \(v\) ,这一次榜首是 \(u\) ,需要满足这样的条件:
如果最后 \(\sum b_i > m\),显然无法构造出合法的解了,否则就把多的全部给榜一。
这样就有了一个 $O(n\times n!) $ 的60分暴力回来路上发现人均拿了60分,就我看错题目爆零的时候心态崩了
不难发现上面的枚举只和上一次的 \(v\) 和 \(b_{v}\) 有关,可以做一个dp。
又要满足 \(b_{u} \geq b_{v}\),这提示我们往差分的方向走。
不难发现,如果 \(b_{1}=x\) ,那么可以等价于当前已经花费了 $ nx $,然后所有的 \(b_{i}=b_{i}-x\) ,那么接下来就与 \(b_{1}\) 无关了。
考虑做一个状压。
\(f_{S,u,k}\) 表示当前选择的集合 \(S\) ,上一次选的是 \(u\) ,当前已经花费了 \(k\) 的方案数是多少。
所以可以得到转移方程:
\(
\begin{aligned}
f_{S\cup v,v,k+d(n-|S|)} &+=f_{S,v,k},v\not \in S
\end{aligned}
\)
最后的答案就是全集 \(S\) 中所有花费小于等于 \(m\) 的方案数。
空间复杂度为 \(O(nm2^n)\),时间复杂度为\(O(n^2m2^n)\)。
Code
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define ll long long
#define ui unsigned int
il ll read(){
bool f=true;ll x=0;
register char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=false;ch=getchar();}
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(f) return x;
return ~(--x);
}
il void write(const ll &x){if(x>9) write(x/10);putchar(x%10+'0');}
il void print(const ll &x) {x<0?putchar('-'),write(~(x-1)):write(x);putchar('\n');}
il ll max(const ll &a,const ll &b){return a>b?a:b;}
il ll min(const ll &a,const ll &b){return a<b?a:b;}
ll a[16];
ll f[1<<13][13][501],base[16],ans,n,m;
il int count(ll x){
int cnt=0;
while(x){
cnt+=x&1;
x>>=1;
}
return cnt;
}
int main(){
ll mxv=0,mxu=0;
n=read(),m=read();
base[0]=1;
for(ri i=0;i<n;++i){
a[i]=read();
if(a[i]>mxv){
mxu=i;
mxv=a[i];
}
}
for(ri i=1;i<=n;++i) base[i]=base[i-1]<<1;
f[0][mxu][0]=1;
for(ri s=0;s<base[n];++s){
int cnt=count(s);
for(ri v=0;v<n;++v){
if(s!=0&&!(s&base[v])) continue;
for(ri u=0;u<n;++u){
if(s&base[u]) continue;
int det=v<u,d=max(a[v]+det-a[u],0);
for(ri l=0;l<=m-d*(n-cnt);++l){
f[s|base[u]][u][d*(n-cnt)+l]+=f[s][v][l];
}
}
}
}
for(ri j=0;j<n;++j){
for(ri i=0;i<=m;++i){
ans+=f[base[n]-1][j][i];
}
}
print(ans);
return 0;
}
D2T3 支配
一眼支配树,刚看到标题就知道了,一开始就没打算写这题。没办法,不会支配树
然后在最后的十几分钟里敲了一个30分的暴力就没管了。
啥时候闲着了再回来补上罢,现在先空着。
2021.12.14
回来补上了。
首先是建出支配树,然后考虑到新加入一条边 \(u,v\) 之后一个点的支配集发生变化当且仅当:
1.\(fa_{x}\) 的支配集发生变化。
2.\(fa_{x}\) 不再支配 \(x\)。
对于两种情况的和,我们找到最潜的 \(p\) 满足 \(p\) 是 \(x\) 的祖先且 \(p\) 的父亲的支配集没有发生变化。
这样就只需要统计所有的 \(siz_{p}\) 即可。
那么考虑什么时候 \(fa_{x}\) 才不会支配 \(x\):
- 删掉 \(x\) 的父亲 \(fa_{x}\) 之后从 \(1\) 能到达 \(u\) 且从 \(v\) 能到达 \(x\)。
那么可以直接求出一个数组 \(f_{x,y}\) 表示删掉 \(fa_{x}\) 之后原图上 \(y\) 能否到达 \(x\),这里直接暴力 dfs
就可以了。
总复杂度 \(O(n^2+nq)\) 。