2022,7 暑假集训
7.11 C 乱搞题
有 \(n\) 个桶,每个桶中装有 \(n\) 个数。保证 \(1 \sim n\) 中的每种数字在所有桶中一共出现恰好 \(n\) 次。
每次操作选择一个区间 \([l,r]\),若满足第 \(l \sim r\) 个桶最上端的数相同,可以将这些桶最上端的数一起取出。
求至少需要多少次操作才能取出所有的数。
保证数据随机
数据范围
\(1 \leq n \leq 1000\)
思路
考虑将相邻两个桶内的相同的数连一条边。可以发现如果两条边不存在交叉,就存在一种方案使得这两组数都可以同时被拿出,也就可以减少两次操作数。而如果想要选择最多不相交的边,不难发现就是求两个序列的最长公共子序列。
一般求 LCS 的方法是dp,时间复杂度为 \(O(n^2)\),但由于本题数据保证随机,可以在dp的基础上优化。
对于第二个序列,记录每一个值出现的位置,并且从后往前遍历第一个序列,查询在第二个序列中以相同字符下标前的数字结尾的公共字串的最大值,可以用树状数组维护。由于数据随机,相邻两列之间相同对数的期望为 \(O(n)\)。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1010;
int a[N][N],n,c[N],ans,f[N];
vector<int>id[N];
int lowbit(int x){return x&-x;}
void update(int x,int k){while(x<=n){c[x]=max(c[x],k);x+=lowbit(x);}}
int query(int x){int res=0;while(x){res=max(res,c[x]);x-=lowbit(x);}return res;}
int solve(int a[],int b[])// LCS
{
for(int i=1;i<=n;i++) id[b[i]].push_back(i);
for(int i=1;i<=n;i++)
{
for(auto j:id[a[i]]) f[j]=query(j-1)+1;
for(auto j:id[a[i]]) update(j,f[j]);
}
int res=query(n);memset(c,0,sizeof(c));
for(int i=1;i<=n;i++) id[i].clear();return res;
}
int main()
{
// freopen("rand.in","r",stdin);
// freopen("rand.out","w",stdout);
scanf("%d",&n);ans=n*n;for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
for(int i=1;i<n;i++) ans-=solve(a[i],a[i+1]);printf("%d\n",ans);
return 0;
}
7.11 D 奇怪题
有 \(n\) 个七位数,记作 \(a_1 \sim a_n\)。
对七位数 \(v\) 的一次位移会将 \(v\) 的最高位移到最低位,其余位相对顺序保持不变。例如 \(1234567\) 在一次位移后会变
成 \(2345671\)。
每次操作可以选定一段区间 \([l,r]\) 和一个整数 \(k\),对 \(a_l \sim a_r\) 分别进行 \(k\) 次位移。
求至少需要多少次操作,才能将所有 \(a_i\) 都调整成 可能的最大值。
数据范围
\(1 \leq n \leq 501\)
思路
首先可以将七位数字都相同的数删去;对于剩下的数,由于 \(7\) 是一个质数,可能的最大值只能在一个位置取到,设为 \(c_i\)。
那么题目就转化为每次选择一个区间,给区间内的每一个数都加上 \(k\),求最小的操作次数使得所有的数在模 \(7\) 意义下都为 \(0\)。
继续考虑差分,先对原序列求一个差分数组,每次的区间加操作就转化为给差分序列中的一个数加上一个数,另一个数减去相同的数。现在就是要用最少的操作次数使得差分序列中的每一个数在模 \(7\) 意义下都为 \(0\)(下面简称为 \(0\))。
对于原差分序列的一个有 \(x\) 个元素的子集,如果这 \(x\) 个元素的和为 \(0\),那么每次对集合中的两个数进行操作不会改变和的大小,那么必然能在 \(x-1\) 次操作内将这些元素都变成 \(0\)。那么现在就是要在满足和为 \(0\) 的情况下,将原差分序列中的 \(n+1\) 的数分成尽可能划分成多的子集。
首先可以将序列中和为 \(0\) 的两个数单独拎出来构成一个集合,这样操作后就剩下之多 \(3\) 种不相同的数。
设 \(f_{i,j,k}\) 表示三种数分别剩余 \(i,j,k\) 个时所需的最少次数。由于转移的方案特别多,可以首先剪去很多劣的方案。对于不劣的方案,显然满足以下两点:
- \(a,b,c<7\)。如果一个数大于 \(7\) 了,那么显然把这 \(7\) 个数单独放一个集合更优。
2.\(gcd(a,b,c)=1\),如果他们的最大公约数不为 \(1\),那么就可以把这个集合拆分成若干个相等的子集,显然不是优的方案。
这样一稿剩余的合法转移方案数就很少了,可以通过本题。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
const int N=521,T=1e7;
const int YY[9]={1111111,2222222,3333333,4444444,5555555,6666666,7777777,8888888,9999999};
int m,id[N],c[N],ans,n,a[N],cnt[N];
short f[N][N][N];
int gcd(int a,int b){return b==0?a:gcd(b,a%b);}
struct node{int a,b,c;}p[N];
int main()
{
// freopen("strange.in","r",stdin);
// freopen("strange.out","w",stdout);
scanf("%d",&n);
for(int x,i=1;i<=n;i++)
{
scanf("%d",&x);int j,maxv=0;
for(j=0;j<9;j++) if(x==YY[j]) break;
if(j!=9) {i--,n--;continue;}
for(j=0;j<7;j++)
{
if(x>maxv) maxv=x,a[i]=j;
x=x*10%T+x*10/T;
}
}
for(int i=n+1;i>=1;i--) a[i]=(a[i]-a[i-1]+7)%7,cnt[a[i]]++;
for(int x,i=1;i<=3;i++) x=min(cnt[i],cnt[7-i]),ans+=x,cnt[i]-=x,cnt[7-i]-=x;
for(int i=1;i<=6;i++) if(cnt[i]) c[++m]=cnt[i],id[m]=i;m=0;
for(int i=0;i<7;i++)
for(int j=0;j<7;j++)
for(int k=0;k<7;k++)
// if((i||j||k)&&(i*id[1]+j*id[2]+k*id[3])%7==0) p[++m]=node{i,j,k};
if((i||j||k)&&(i*id[1]+j*id[2]+k*id[3])%7==0&&gcd(gcd(i,j),k)==1) p[++m]=node{i,j,k};
memset(f,0x3f,sizeof(f));f[0][0][0]=0;
for(int i=0;i<=c[1];i++)
for(int j=0;j<=c[2];j++)
for(int k=0;k<=c[3];k++)
for(int t=1;t<=m;t++)
if(i>=p[t].a&&j>=p[t].b&&k>=p[t].c)
f[i][j][k]=min(f[i][j][k],f[i-p[t].a][j-p[t].b][k-p[t].c]+p[t].a+p[t].b+p[t].c-1);
printf("%d\n",f[c[1]][c[2]][c[3]]+ans);
return 0;
}
7.13 C 终极方方方练习题
玩完游戏后,FXT 和 ZJ 终于想起了仍然被隔离的九条可怜。
所以他们开始继续研究这些石子,终于发现了可怜留下的信息。
原来,通过可怜的秘法可以构建出一个三维空间,里面共有 \(A*B*C\) 个整点 (\(1 \leq a \leq A\) ,\(1 \leq b \leq B\)
,\(1 \leq c \leq C\) )。
他们将 \(n\) 颗石子依次抛入三维空间之中,第 \(i\) 颗石子落在了 \((x_i,y_i,z_i)\) 的位置上,且 \((x_i,y_i,z_i)\) 将会变成关键点。
定义一个点 \(P\) 会被一个点 \(Q\) 压制,当且仅当存在至少两维,满足 \(P\) 该维的坐标严格小于 \(Q\) 该维的坐标。
而他们所需要做的,就是求出一共有多少个点,压制了所有标记点。
他们相信只要完成了这个任务就能获知可怜留下的终极信息。由于时间紧迫,只好来向你求助。
数据范围
\(1 \leq n,A,B,C \leq 5 \times 10^5\)
思路
考虑求出不能压制所有标记点的点数。
对于一个点 \((x,y,z)\),不能压制它的点的范围在 \((1 \sim x,1\sim y,1\sim C),(1\sim x,1\sim B,1\sim z),(1\sim A,1\sim y,1\sim z)\) 内。这三个集合必然存在交集,考虑去重。注意转化后接下来的点就要满足三个维度被全面压制了。
可以将一个不能压制的点的范围看成是一个询问,将所有的询问按照 \(x\) 这一维从大到小排序,并且用线段树维护 \(y\) 和 \(z\) 这两维的信息。
具体地,从大到小枚举第一维坐标,假设当前第一维枚举到了 \(x\),坐标在这一维上的点就必然不能在 \(x\) 轴上压制更大的点,现在只需要在 \(y\) 和 \(z\) 上被任意一个询问压制就可以了。那么对于一组 \((y,z)\),对于 \(b<=y\) 的点,只需要满足 \(c<=z\) 即可,由于只需要满足被一个询问压制,就只需要在所有的 \(z\) 中取最大值即可。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
inline int read()
{
int s=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();}return s*f;
}
const int N=5e5+10,M=N*3;
#define LL long long
struct node{int x,y,z;bool operator <(const node &t)const{return x>t.x;};}a[M];
int n,m,A,B,C;LL ans;
struct tree{LL sum,minv,tag;}tr[N<<2];
void push_up(int p){tr[p].minv=min(tr[p<<1].minv,tr[p<<1|1].minv),tr[p].sum=tr[p<<1].sum+tr[p<<1|1].sum;}
void push_down(int p,int l,int r)
{
if(!tr[p].tag) return ;int mid=l+r>>1;
tr[p<<1].minv=tr[p<<1|1].minv=tr[p<<1].tag=tr[p<<1|1].tag=tr[p].tag;
tr[p<<1].sum=tr[p].tag*(mid-l+1);tr[p<<1|1].sum=tr[p].tag*(r-mid);tr[p].tag=0;
}
void cover(int p,int l,int r,int x,int y)
{
if(r<=x) {tr[p].tag=tr[p].minv=y,tr[p].sum=1ll*(r-l+1)*y;return ;}
int mid=l+r>>1;push_down(p,l,r);
if(x<=mid) cover(p<<1,l,mid,x,y);
else tr[p<<1].tag=tr[p<<1].minv=y,tr[p<<1].sum=1ll*(mid-l+1)*y,cover(p<<1|1,mid+1,r,x,y);
push_up(p);
}
/*
void cover(int x,int y,int l,int r,int rt){
if(r<=x){return pushcov(rt,y,r-l+1);}
int mid=(l+r)>>1;
pushdown(rt,mid-l+1,r-mid);
if(x<=mid)cover(x,y,l,mid,rt<<1);
else pushcov(rt<<1,y,mid-l+1),cover(x,y,mid+1,r,rt<<1|1);pushup(rt);
}
*/
void update(int p,int l,int r,int x,int y)
{
if(y<=tr[p].minv) return;if(l==r) {tr[p].sum=tr[p].minv=tr[p].tag=y;return ;}
push_down(p,l,r);int mid=l+r>>1;
if(x<=mid) update(p<<1,l,mid,x,y);
else if(y<=tr[p<<1].minv) update(p<<1|1,mid+1,r,x,y);
else cover(p<<1|1,mid+1,r,x,y),update(p<<1,l,mid,x,y);
push_up(p);
}
int main()
{
n=read(),A=read(),B=read(),C=read();
for(int i=1;i<=n;i++)
{
int x=read(),y=read(),z=read();
a[++m]=node{x,y,C};a[++m]=node{x,B,z};a[++m]=node{A,y,z};
}
sort(a+1,a+m+1);
for(int i=A,now=1;i>=1;i--)
{
while(now<=m&&a[now].x>=i) update(1,1,B,a[now].y,a[now].z),now++;ans+=tr[1].sum;
}
printf("%lld\n",1ll*A*B*C-ans);
return 0;
}
7.14 B 最短路
给定一张 \(n\) 个点 \(m\) 条边的带权无向连通图 \(G\),以及一个大小为 \(k\) 的关键点集合 \(A\). 有个人要从点 \(s\)
走到点 \(t\),你将对所有边都加上一个非负整数 \(c\). 求最大的 \(c\),使得加上 \(c\) 后,满足:\(s\) 到 \(t\) 的最短路 \(=s\) 到
\(t\) 且只能经过 \(A\) 中点的最短路
数据范围
\(1 ≤ n ≤ 100, 1 ≤ m ≤ 10000, 1 ≤ ci ≤ 10^9\)
思路
首先由于无法确定在给边权加上 \(c-1\) 时只经过关键点的最短路为整体最短路,二分答案不能保证正确性。
由于是给所有边加上一个权值,那么最短路肯定和经过的边数有关。考虑设 \(f[i][j]\) 为只经过关键点到达 \(i\) 用了 \(j\) 条边的最短路径长度。\(g[i][j]\) 为经过任意点到达 \(i\) 用了 \(j\) 条边的最短路径长度。这两个数组可以先 \(O(n^2)\) 求出。不难发现 \(g[i][j] \leq f[i][j]\)。
设 \((x,n)\) 表示走过 \(n\) 条边,只经过关键点到达终点的最短路径长度为 \(x\);\((y.m)\) 表示走过 \(m\) 条边,经过任意点到达终点的最短路径长度为 \(y\)。现在要让 \((x,n)\) 成为最短路,可以推出 \(cn+x \leq cm+y\),化简得到 \(c=\dfrac{y-x}{n-m}\)。
从小到大枚举经过的边数 \(i\)(就是枚举哪一条路径作为最短路),对于所有的 \(g[t][i]\) 求出 \(c\),并取最大值就是答案。如果最大值小于 \(0\),那么就无解,如果 \(n=m\) 则有无穷解。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#pragma GCC optimize(2)
using namespace std;
#define LL long long
inline int read()
{
int s=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();}return s*f;
}
const int N=1010,M=2e5+10;
const LL INF=1e18;
int n,m,S,T,k,ins[N],h[N],idx;
struct edge{int v,w,nex;}e[M];
void add(int u,int v,int w){e[++idx]=edge{v,w,h[u]};h[u]=idx;}
LL f[N][N],g[N][N];
inline void solve()
{
n=read(),m=read(),S=read(),T=read();LL ans=-1;
memset(h,0,sizeof(h));memset(ins,0,sizeof(ins));idx=0;
for(register int u,v,w,i=1;i<=m;i++) u=read(),v=read(),w=read(),add(u,v,w),add(v,u,w);k=read();
for(register int i=1;i<=k;i++) ins[read()]=1;
for(register int i=1;i<=n;i++) for(register int j=0;j<=n;j++) f[i][j]=g[i][j]=INF;f[S][0]=g[S][0]=0;
for(register int j=0;j<n;j++)
for(register int i=1;i<=n;i++)
if(f[i][j]<INF&&ins[i])
for(int k=h[i];k;k=e[k].nex)
f[e[k].v][j+1]=min(f[e[k].v][j+1],f[i][j]+e[k].w);
for(register int j=0;j<n;j++)
for(register int i=1;i<=n;i++)
if(g[i][j]<INF)
for(int k=h[i];k;k=e[k].nex)
g[e[k].v][j+1]=min(g[e[k].v][j+1],g[i][j]+e[k].w);
for(int i=1;i<=n;i++) printf("%lld ",f[T][i]);puts("");
for(register int i=1;i<=n;i++)
{
if(f[T][i]==INF||g[T][i]<f[T][i]) continue;
int j=1;
while(j<i&&g[T][j]==INF) j++;
if(j==i) {ans=INF;break;}
LL cur=INF;
for(j=1;j<i;j++) cur=min(cur,(g[T][j]-f[T][i])/(i-j));
for(j=i+1;j<=n;j++) if(f[T][i]+cur*i>g[T][j]+cur*j) break;
if(j<=n) continue;ans=max(ans,cur);
}
if(ans==-1) puts("Impossible");
else if(ans==INF) puts("Infinity");
else printf("%lld\n",ans);
}
int main()
{
freopen("path.in","r",stdin);
freopen("path.out","w",stdout);
int TT=read();while(TT--) solve();
return 0;
}
17 B 树
注意到题目描述中每种颜色的数量不超过 \(20\) 那么就可以枚举每一种颜色的点,考虑它们对于答案的贡献。
可以先把树上的点转化为 \(dfn\) 序成为序列上的问题。对于一对颜色相同的点对 \((u,v)\)。如果 \(u\) 为 \(v\) 的祖先,那么选择 \(v\) 子树内的点就不能选择 \(u\) 子树以外的点;对于一般的情况,选择 \(u\) 子树中的节点就不能选择 \(v\) 中的节点。
可以把这些限制条件转化为类似于扫描线上的加减。用一个线段树来维护当前每一个节点的状态 \(0/1\),表示能选或不能选。在新的限制条件进来时更新一下即可。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define LL long long
const int N=2e5+10,M=4e6+10;
LL ans;int a[N],dep[N],n,m,top[N],h[N],idx,pos[N],dfn[N],siz[N],son[N],L[N],R[N],fa[N],num;
vector<int>b[N];
struct edge{int v,nex;}e[N];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
struct opt{int l,r,x,sign;bool operator <(const opt &t)const{return x<t.x;};}q[M];
void dfs1(int u,int Fa)
{
siz[u]=1;dep[u]=dep[Fa]+1;fa[u]=Fa;
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;if(v==Fa) continue;
dfs1(v,u);siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int t)
{
top[u]=t,L[u]=++num;pos[num]=u;if(son[u]) dfs2(son[u],t);
for(int i=h[u];i;i=e[i].nex) if(e[i].v!=son[u]&&e[i].v!=fa[u]) dfs2(e[i].v,e[i].v);
R[u]=num;
}
int LCA(int x,int y)
{
while(top[x]!=top[y]) {if(dep[top[x]]<dep[top[y]]) swap(x,y);x=fa[top[x]];} return dep[x]<dep[y]?x:y;
}
int getfa(int u,int k){ while(dep[u]-dep[top[u]]<k) k-=dep[u]-dep[top[u]]+1,u=fa[top[u]];return pos[L[u]-k];}
void modify(int a,int b,int x,int y)
{
// printf("%d %d %d %d\n",a,b,x,y);
q[++m]=opt{x,y,a,1};q[++m]=opt{x,y,b+1,-1};
q[++m]=opt{a,b,x,1};q[++m]=opt{a,b,y+1,-1};
}
struct tree{int cnt,tag;}tr[N<<2];
/*void push_up(int p){tr[p].cnt=tr[p<<1].cnt+tr[p<<1|1].cnt;}
void push_down(int p,int l,int r)
{
if(!tr[p].tag) return ;int mid=l+r>>1;
tr[p<<1].cnt=(mid-l+1)*min(tr[p].tag,1);tr[p<<1|1].cnt=(r-mid)*min(tr[p].tag,1);tr[p].tag=0;
}*/
void push_up(int p,int l,int r)
{
if(tr[p].tag) tr[p].cnt=r-l+1;else if(l<r) tr[p].cnt=tr[p<<1].cnt+tr[p<<1|1].cnt;else tr[p].cnt=0;
}
void update(int p,int l,int r,int L,int R,int k)
{
if(L<=l&&r<=R) {tr[p].tag+=k;push_up(p,l,r);return ;} int mid=l+r>>1;
if(L<=mid) update(p<<1,l,mid,L,R,k);if(R>mid) update(p<<1|1,mid+1,r,L,R,k);push_up(p,l,r);
}
/*void update(int p,int l,int r,int L,int R,int k)
{
if(L<=l&&r<=R) {tr[p].tag+=k;tr[p].cnt=(r-l+1)*min(tr[p].tag,1);return ;} push_down(p,l,r);int mid=l+r>>1;
if(L<=mid) update(p<<1,l,mid,L,R,k);if(R>mid) update(p<<1|1,mid+1,r,L,R,k);push_up(p);
}*/
int main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%d",&a[i]),b[a[i]].push_back(i);
for(int u,v,i=1;i<n;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);dfs1(1,0),dfs2(1,1);
// for(int i=1;i<=n;i++) printf("%d %d\n",L[i],R[i]);return 0;
for(int i=1;i<=n;i++)
for(int j=0;j<b[i].size();j++)
for(int k=0;k<j;k++)
{
int u=b[i][j],v=b[i][k],z=LCA(u,v);
if(dep[u]>dep[v]) swap(u,v);
if(u==z)
{
z=getfa(v,dep[v]-dep[u]-1);
if(L[z]>1) modify(1,L[z]-1,L[v],R[v]);
if(R[z]<n) modify(R[z]+1,n,L[v],R[v]);
} else modify(L[u],R[u],L[v],R[v]);
}
sort(q+1,q+m+1);
for(int i=1,j=0;i<=n;i++)
{
while(j+1<=m&&q[j+1].x<=i) j++,update(1,1,n,q[j].l,q[j].r,q[j].sign);
ans+=tr[1].cnt;
}
printf("%lld\n",(1ll*n*(n+1)-ans)/2);
return 0;
}