2025杭电暑期联赛第一场 (持续更新)
10
题意:给定一个排列,对于每一个1<=i<=j<=n,当(j-i+1)为奇数时,求和ixjxmid(a[i]~a[i])
思路:
注意到n<=2000,可以使用暴力枚举加主席树维护区间第k小解决
struct PT{
static constexpr int N=2e5+5;
int cntNodes,root[N];
struct node{
int l,r;
int cnt;
}tr[4*N+17*N];
void modify(int &u,int v,int l,int r,int x){
u=++cntNodes;
tr[u]=tr[v];
tr[u].cnt++;
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)modify(tr[u].l,tr[v].l,l,mid,x);
else modify(tr[u].r,tr[v].r,mid+1,r,x);
}
int kth(int u,int v,int l,int r,int k){
if(l==r)return l;
int res=tr[tr[v].l].cnt-tr[tr[u].l].cnt;
int mid=l+r>>1;
if(k<=res)return kth(tr[u].l,tr[v].l,l,mid,k);
else {
return kth(tr[u].r,tr[v].r,mid+1,r,k-res);
}
}
}tree;
void solve(){
int n;cin>>n;
int cnt=0;
vector<int>a(n+1);
rep(i,1,n){
cin>>a[i];
tree.modify(tree.root[i],tree.root[i-1],1,n,a[i]);
}
int ans=0;
rep(i,1,n){
rep(j,i,n){
if((j-i+1)&1){
ans+=i*j*tree.kth(tree.root[i-1],tree.root[j],1,n,(j-i+2)/2);
}
}
}
cout<<ans<<endl;
}
09
题意:给定排列,求最长子序列使得两端值大于中间所有值
思路:
这题比赛时想了太久
首先可以想到,为了使答案尽可能大要使 答案选取的两端值比较大且距离相距比较远
发现不好枚举两个端点,或者枚举一个端点另一个端点二分的做法都是行不通的
不妨直接按大小枚举端点值
(答案取决于两个端点值中比较小的那一个)
设置两个指针l和r,根据端点值尽可能往外扩展
显然答案为l和r确定的区间长度减去该区间已经枚举过的更大的端点值
int n;
void solve(){
cin>>n;
if(n==1||n==2){
cout<<n<<endl;return;
}
vector<int>a(n+1);
vector<int>pos(n+1);
rep(i,1,n){
cin>>a[i];
pos[a[i]]=i;
}
int l=-1,r=-1;
l=r=pos[n];
int ans=0;
for(int i=n-1;i>=1;i--){
int lt=l,rt=r;
l=min(l,pos[i]);
r=max(r,pos[i]);
if(l<lt||r>rt){
ans=max(ans,r-l+1-(n-i-1));
}
}
cout<<max(2ll,ans)<<endl;
}
05
题意:给定一个图,图每条边有一个协会,如果遍历到上一条边的协会和该边的协会相同则无需花费,否则花费一元。
求1到n的最短路
思路:
Dj可以通过,需要额外记录一下上一条边的协会种类
并且不需要vis数组,否则wa
需要优化当dis>=d[n]时break,否则MLE
int n,m;
vector<pii>e[maxn];
int d[maxn];
int vis[maxn];
struct node{
int dis;
int to;
int last;
bool operator<(const node&A)const{
return dis>A.dis;
}
};
priority_queue<node>pq;
void dj(int s){
rep(i,1,n)d[i]=0x3f3f3f3f;
d[s]=0;
pq.push((node){0,s,0});
while(pq.size()){
auto[dis,now,last]=pq.top();pq.pop();
if(dis>=d[n])break;
for(auto[to,xiehui]:e[now]){
if(d[to]>=dis&&xiehui==last){
d[to]=dis;
pq.push((node){dis,to,xiehui});
}else{
if(d[to]>=dis+1){
d[to]=dis+1;
pq.push((node){dis+1,to,xiehui});
}
}
}
}
}
void solve(){
cin>>n>>m;
rep(i,1,n)e[i].clear();
rep(i,1,n)d[i]=vis[i]=0;
while(pq.size())pq.pop();
rep(i,1,m){
int u,v,c;cin>>u>>v>>c;
e[u].pb({v,c});
e[v].pb({u,c});
}
dj(1);
cout<<d[n]<<endl;
}
05
题意:给定一个nxm的矩阵,每个矩阵点有权值。游客可以从高权值点走向低权值点,游客从(1,1)开始,需要遍历所有矩阵点
可搭建传送门,传送门互通,需要花费一定代价。初始传送门在(1,1)处
思路:
花费=传送门数量+搭建线路花费
显然峰值矩阵点必须要搭建传送门
因此传送门数量确定
为了使搭建线路花费最小
已知搭建线路花费114x(x1-x2)+5141x(y1-y2)+919810(ax-ay)
显然x1-x2和y1-y2的差值就算最大也比不上ax-ay差值多1
因此将峰值点按矩阵值大小排列
挨个连接形成最小生成树即可
int g[105][105];
int vis[105][105];
const int c1=114;
const int c2=5141;
const int c3=919810;
const int c4=(1ll<<34);
struct node{
pii pos;
int val;
bool operator<(const node &A){
return val<A.val;
}
};
int cal(pii a,pii b){
int res=0;
res+=c1*abs(a.fi-b.fi);
res+=c2*abs(a.se-b.se);
res+=c3*abs(g[a.fi][a.se]-g[b.fi][b.se]);
return res;
}
int n,m;
const int dx[]={1,-1,0,0};
const int dy[]={0,0,1,-1};
void dfs(int x,int y){
vis[x][y]=1;
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||nx>n||ny<1||ny>m)continue;
if(g[x][y]<g[nx][ny])continue;
dfs(nx,ny);
}
}
void solve(){
cin>>n>>m;
memset(vis,0,sizeof vis);
// memset(g,0,sizeof g);
vector<node>qr;
rep(i,1,n)rep(j,1,m){
cin>>g[i][j];
node A;A.pos={i,j};
A.val=g[i][j];
qr.pb(A);
}
sort(qr.begin(),qr.end());
int M=qr.size();
int cnt=0;//传送门数量
int cost=0;//建设成本
int ans=0;
vector<node>vv;
node A;A.val=g[1][1];
A.pos={1,1};
vv.pb(A);
rep(i,1,n){
rep(j,1,m){
if(i==1&&j==1)continue;
int ok=0;
for(int z=0;z<4;z++){
int nx=i+dx[z],ny=j+dy[z];
if(nx<1||nx>n||ny<1||ny>m)continue;
if(g[nx][ny]>g[i][j]){
ok=1;
}
}
if(!ok){
node A;A.val=g[i][j];A.pos={i,j};
vv.pb(A);
}
}
}
cnt=vv.size()-1;
sort(vv.begin(),vv.end());
for(int i=1;i<vv.size();i++){
pii pos_last=vv[i-1].pos;
pii pos_now=vv[i].pos;
cost+=cal(pos_last,pos_now);
}
ans+=cost;
ans+=cnt*c4;
cout<<ans<<endl;
}
07
树上LCM
题意:
给定x和一棵带点权值树,求有多少简单路径的lcm为x
思路:
1.
先看哪些权值累次lcm能得到x
我们知道累次lcm,即把这些数的最高次质因子类乘
显然这些权值必须为x的约数才有可能使lcm等于x,否则必定不行
2.
其次看树的简单路径是如何构成的
我们可以枚举每一个点作为简单路径的LCA
那么满足它为LCA的简单路径数量 = 单独它一个点 + 子树一条链的数目 + 不同的子树两条链构成的简单路径
由于x的大小比较小,为1e7,因此可以发现它的质因子种类数一定不超过7
记x的质因子种类有k种
可以将每个点的权值压缩成一个k位的二进制。让某一位为1,当且仅当它的该种质因子个数与x相当
特判当点权值不为x的约数,即经过它的简单路径的lcm一定不为x时,记为-1
那么对于若干个点权值构成的lcm,只要它们按位或起来等于2^k-1,就满足lcm=x的条件
接下来通过DFS,从底至顶的进行树形DP
特判a[u]如果为-1,则直接return
记dp[u][state]为以u为LCA,经过u的子链状态为state的个数
初始化dp[u][a[u]]=1
显然如果此时暴力枚举它的两条子链的state,复杂度将会是O(nx(2^k)x(2^k))约等于1e9
所以最多只能枚举一条子链的状态,另一条链用超集和优化
先从左往右枚举子链的状态i,它的个数为dp[v][i],即当前子树中有的数量
让a[u]也累次到lcm。令X=i|a[u],再将X取反。为了满足lcm=x,另一条子链的状态就需要是此时X的超集
求超集的数量,需要用高维后缀和。复杂度为O(kx2^k)的循环
求出当前枚举过的子树的不同状态子链的个数
同时进行dp的状态转移.
当a[u]=x,即为简单路径为单独一个点的时候,ans需要增1
(ps:刚开始提交的时候不知道为什么TLE,还以为这题卡常。后来发现是求质因子时把剩余的x=1的情况也加上去了,:(
int lcm(int a,int b){
return a*b/__gcd(a,b);
}
vector<int>e[maxn];
int ans;
int k;
inline void dfs(int u,int fa,vector<int>&a,vector<vector<int>>&dp){
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u,a,dp);
}
if(a[u]==-1)return;
dp[u][a[u]]=1;
for(int v:e[u]){
if(v==fa)continue;
vector<int>S=dp[u];//计算每个状态的超集和(SOSDP)
for(int i=0;i<k;i++){
for(int j=0;j<(1ll<<k);j++){
if((j&(1ll<<i))==0){
S[j]+=S[j|(1ll<<i)];
}
}
}
//u点作为LCA
//枚举子树的一条链(状态i),另一条链利用超集和优化枚举
//顺便进行dp转移
for(int i=0;i<(1ll<<k);i++){
int X=i|a[u];
int res=S[X^((1ll<<k)-1)]*dp[v][i];
ans+=res;
dp[u][X]+=dp[v][i];
}
}
if(a[u]==((1ll<<k)-1))ans++;
}
void solve(){
int n,x;cin>>n>>x;
ans=0;
k=0;
rep(i,1,n)e[i].clear();
rep(i,1,n-1){
int u,v;cin>>u>>v;
e[u].pb(v);e[v].pb(u);
}
map<int,int>mp;
int tmp=x;
for(int j=2;j*j<=x;j++){
int cnt=0;
while(tmp%j==0){
cnt++;
tmp/=j;
}
if(cnt)mp[j]=cnt;
}
if(tmp>1)mp[tmp]=1;
k=mp.size();
vector<int>a(n+1);rep(i,1,n){
cin>>a[i];
if(lcm(a[i],x)!=x){a[i]=-1;continue;}
int val=0;
int t=0;
for(auto[prime,nums]:mp){
int c=0;
while(a[i]%prime==0){
c++;
a[i]/=prime;
}
if(c==nums){
val|=(1ll<<t);
}
t++;
}
a[i]=val;
}
vector<vector<int>>dp(n+1,vector<int>(1ll<<k,0));
dfs(1,-1,a,dp);
cout<<ans<<endl;
}
A
博弈
题意:给定n个房间,每个房间有任意堆石子,每堆石子任意个。两人进行nim游戏
但是只能当一个房间石子取完了才能去下一个房间
求有多少个房间排列能使得先手必胜
思路:
多得不整,别的不唠。先知道有关Nim博弈的神秘小结论
对于Nim游戏:(先取完石子的获胜)

对于反Nim游戏:(先取完石子的失败)

发现不论是Nim还是反Nim,只要第一个房间Nim和(石子数量的异或和)不为0,那么先手在第一个房间拥有必胜策略
进而可以说,此时先手可以控制这个房间的胜负从而控制自己下回合是先手还是后手
显然
- 如果下一个房间Nim和不为0,先手就让自己在这个房间输,从而使自己下一个房间先手
- 如果下一个房间Nim和为0,先手就让自己在这个房间赢,从而使自己下一个房间后手
一直持续到最后一个房间,发现先手这样的策略是必胜的
为了让Alice赢,因此只需挑出第一个房间Nim和不为0,剩余的交给Alice操作就好了
除此之外,需要注意可能有房间有奇数个都是1的石子堆,有房间有偶数个都是1的石子堆
显然偶数的石子堆无论如何都不会改变先后顺序,因此先不讨论
而奇数个奇数堆个1的房间会导致先后手顺序变化,偶数个奇数堆个1的房间则不会
所以关键在第一个房间的Nim和,以及在此之前奇数堆个1的房间的个数
不妨开头枚举奇数个堆的房间个数
- 如果奇数个房间,那么变为Bob先手,第一个就要挑Nim和为0的房间
- 如果偶数个房间,那么还是Alice先手,第一个挑Nim和为偶数的房间即可
最后进行排列组合,不要忘记把偶数堆个1的房间也排列进去。
(其他的排好后假设个数为n个,那么空隙有n+1个,此时放1个偶数堆房间进去。然后空隙变为n+2个,以此类推)
同时取模也要注意(才发现c++乘号和取模号运算优先级其实相同:D)
int fact[maxn],infact[maxn];
int ksm(int a,int b){
int res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res%mod;
}
int inv(int x){return ksm(x,mod-2);}
void init(){
fact[0]=infact[0]=1;
rep(i,1,1e6){
fact[i]=fact[i-1]*i%mod;
infact[i]=infact[i-1]*inv(i)%mod;
}
}
int C(int n,int r){
if (n < 0 || r < 0 || n < r) return 0;
return (((fact[n]*infact[n-r])%mod)*infact[r])%mod;
}
void solve(){
int n;cin>>n;
int c0=0,c1=0,c2=0,c3=0;
//c0:全1且有偶数个1的房间个数
//c1:全1且有奇数个1的房间个数
//c2:非全1且nim和不为0的房间个数
//c3:非全1且nim和为0的房间个数
rep(i,1,n){
int k;cin>>k;
int nim=0;
int flag=1;
rep(j,1,k){
int x;cin>>x;
if(x>1)flag=0;
nim^=x;
}
if(flag){
//全1
if(nim){//奇数个1
c1++;
}else{//偶数个1
c0++;
}
}else{
if(nim){
c2++;
}else{
c3++;
}
}
}
if(c2+c3==0){
//只有全1的房间
if(c1&1){
cout<<fact[n]<<endl;
}else cout<<0<<endl;
return;
}
int ans=0;
//先不看全1且为偶数个的房间
//那么剩下的是c1+c2+c3
//枚举最前面连续个全1且奇数个1的房间个数
for(int i=0;i<=c1;i++){
if(i&1){
//Bob先手- >接着要求房间nim和为0
ans+=C(c1,i)*fact[i]%mod*c3%mod*fact[(c1-i)+c2+(c3-1)]%mod;
ans%=mod;
}else{
//Alice先手->接着要求房间nim和非0
ans+=C(c1,i)*fact[i]%mod*c2%mod*fact[(c1-i)+(c2-1)+c3]%mod;
ans%=mod;
}
}
//最后把全1偶数房间插入
//最初有n-c0+1个空格
for(int i=1;i<=c0;i++){
ans*=(n-c0+i);
ans%=mod;
}
cout<<ans<<endl;
}
1002
题意:给定一排个数为n的金矿,金矿每天产出ai金币,从左往右开始收金币。每个金矿有一个怪物,每次遇到怪物会消耗min(ai,bi)的金币
一共有m天
- 操作1:永久修改一个金矿的ai
- 操作2:永久修改一个怪物的bi
- 操作3:回溯到第x天
- 操作4:k个哥布林变得穷凶极恶,遇到它们会收取[sum/2]的金币数,然后变回原来状态
对于每一个操作4的天,输出这天给与怪物的金币
思路:
第一次看这题感觉收金币的过程是线性的,不知道怎么用线段树维护
不妨先单独考虑每一个点的情况 , 发现怪物收取的金币,也就是对答案的贡献是:现在拥有的金币数 和 bi 的min
合并,那么如果前面有多出来的金币,就可以给后面的怪物,也就是左节点可以为右节点产生贡献
对于区间信息Info,我们需要知道
这个线段剩余的金币数量
这个线段怪物所还需要的金币数量
对答案的贡献
区间合并,就是考虑左区间剩余金币对右区间的影响
考虑到操作3回溯,需要把问题转化为事件图
设置每一个线段树状态为一个结点
只要知道这个状态所代表的天数就可以计算答案
对于操作过程,即是树的dfs
对于操作1,2可以单点修改
对于操作4,由于不是永久性的,直接线段树区域查询分块暴力
int a[maxn],b[maxn];
int n;
template<class Info> struct LazySegmentTree{
vector<Info> info;
// vector<Tag> tag;
LazySegmentTree(){
info.assign(n<<2,Info());
// tag.assign(n<<2,Tag());
function<void(int ,int ,int)>build =[&](int p,int l,int r){
info[p].l = l;info[p].r = r;
if (l==r){
int add = min(a[l-1],b[l-1]);
info[p].left = a[l-1] - add;
info[p].need = b[l-1] - add;
info[p].ans =add;
return;
}
int mid =l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pull(p);
};
build(1,1,n);
}
void pull(int p){
info[p] =info[p<<1] + info[p<<1|1];
}
// void apply(int p, const Tag& t) {
// info[p].apply(t);
// tag[p].apply(t);
// }
// void push(int p){}
// void rangeApply(int p,int l,int r,int x,int y,const Tag&v){
// if(l >y || r< x)return;
// if(l>=x&&r<=y){
// return;
// }
// // push(p);
// int mid= l+r>>1;
// rangeApply(p<<1,l,mid,x,y,v);
// rangeApply(p<<1|1,mid+1,r,x,y,v);
// pull(p);
// }
// void rangeApply(int l,int r,const Tag&v){
// return rangeApply(1,1,n,l,r,v);
// }
void pointApply(int p,int opt,int pos,int x){
int l =info[p].l, r=info[p].r;
if(l==r&&l==pos){
if(opt==1){
a[pos-1] = x;
}else if(opt==2){
b[pos-1] = x;
}
int add =min(a[pos-1],b[pos-1]);
info[p].left = a[pos-1] -add;
info[p].need = b[pos-1] -add;
info[p].ans = add;
return;
}
int mid= l + r >>1;
if(pos<=mid)pointApply(p<<1,opt,pos,x);
else pointApply(p<<1|1,opt,pos,x);
pull(p);
}
Info pointQuery(int p,int pos){
int l = info[p].l;
int r = info[p].r;
if(l == r && l==pos)return info[p];
int mid = l + r>>1;
if(pos<=mid)return pointQuery(p<<1,pos);
else return pointQuery(p<<1|1,pos);
}
Info rangeQuery(int p,int l,int r,int x,int y){
if(l>y || r<x)return Info();
if(l >=x && r <=y)return info[p];
// push(p);
int mid = l + r >>1;
return rangeQuery(p<<1,l,mid,x,y) + rangeQuery(p<<1|1,mid+1,r,x,y);
}
Info rangeQuery(int l,int r){
return rangeQuery(1,1,n,l,r);
}
};
struct Info{
int l,r;
int left;
int need;
int ans;
Info(int left=0,int need=0,int ans=0,int l=0,int r=0){
this ->left =left;
this ->need =need;
this ->ans =ans;
this ->l =l;
this ->r =r;
}
};
Info operator + (const Info &a,const Info &b){
int add =min(a.left,b.need);
Info res = Info(a.left+b.left-add,a.need+b.need-add,a.ans+b.ans+add,a.l,b.r);
return res;
}
struct Edge{
int opt;
int x;
int y;
int nextday;
};
void solve(){
int m;cin>>n>>m;
rep(i,0,n-1)cin>>a[i];
rep(i,0,n-1)cin>>b[i];
vector<vector<Edge>>e(m+1);
vector<int>vis(m+1);
vector<vector<int>>angry(m+1);
auto seg = LazySegmentTree<Info>();
rep(i,1,m){
Edge edge;
auto &[opt,x,y,nextday] = edge;
cin>>opt;
nextday = i;
if(opt<=2){
cin>>x>>y;
e[i-1].pb(edge);
}else if(opt==3){
cin>>x;
e[x].pb(edge);
}else{
e[i-1].pb(edge);
vis[i] = 1;
int k;cin>>k;
rep(j,1,k){
int xt;cin>>xt;
angry[i].pb(xt);
}
}
}
vector<int>ans(m+1);
auto dfs = [&](auto&&self,int cur)->void{
if(vis[cur]){
Info merge;
int L = 1, R;
for(int i = 0; i < angry[cur].size(); i++) {
int pos = angry[cur][i];
R = pos - 1;
merge = merge + seg.rangeQuery(L, R);
merge.left += a[pos-1];
merge.ans += (merge.left + 1) / 2;
merge.left /= 2;
L = pos + 1;
}
merge = merge + seg.rangeQuery(L, n);
ans[cur] = merge.ans;
}
for(auto &[opt,x,y,nextday]:e[cur]){
int prea,preb;
if(opt<=2){
prea = a[x-1];
preb = b[x-1];
seg.pointApply(1,opt,x,y);
int res = seg.rangeQuery(1,n).ans;
}
self(self,nextday);
if(opt<=2){
if(opt==1){
seg.pointApply(1,opt,x,prea);
}
if(opt==2){
seg.pointApply(1,opt,x,preb);
}
}
}
};
dfs(dfs,0);
rep(i,1,m){
if(ans[i])cout<<ans[i]<<endl;
}
}
UPD ON 10/10
更新一手怎么说,把考SA和位运算的补了
1003
题意:
给出一个长度为n的字符串s,和一个参数len
对于所有长度大于等于len的子串,要求至少有一对回文字符
现在可以将这17个字符加个变换buff,即花费wi的代价使较小的字符变为大于等于它的字符
求使条件满足的最小代价
思路:
考虑是否有神秘单调性
发现题目只需要考虑偶数长度的子串,且当长度\(n\)的字符串满足条件,那么一定会使长度为\(m\)(\(n \leq m\))的子串满足条件。因为中间长度为\(n\)的子串会有一对回文字符
现在只需要考虑长度为len的子串(len为奇数时++)
那么对于每一个长度为len的子串:
如果已经有了一对回文字符,那么无需花费任何代价,跳过即可
否则,标记 可以通过加buff使得条件满足的字符(一对对称字符中较小的那个)
最后对于所有二进制集合:
判断这个集合和所有的子串标记的集合是否有交集(只要有一个元素相交即可)
取最小
int n;
string s;
int w[17];
int cost[maxn];
void solve(){
cin>>n>>s;
memset(w,0,sizeof w);
rep(i,0,16)cin>>w[i];
memset(cost,0,sizeof cost);
for(int i=0;i<(1ll<<17);i++){
for(int j=0;j<17;j++){
if((1ll<<j)&i)cost[i]+=w[j];
}
}
int len;cin>>len;
if(len&1)len++;
s=" "+s;
int res=0;
vector<int>vec;
for(int i=1;i<=n-len+1;i++){
int mask=0;
for(int j=0;j<=len/2;j++){
if(s[i+j]==s[i+len-1-j]){
mask=0;break;
}else{
int I=s[i+j]-'a';
int J=s[i+len-1-j]-'a';
mask|=(min(1ll<<I,1ll<<J));
}
}
if(mask)vec.pb(mask);
}
if(!vec.size()){
cout<<0<<endl;return;
}
int ans=1e17;
for(int i=0;i<(1ll<<17);i++){
int ok=1;
for(auto mask:vec){
if(!(i&mask)){
ok=0;break;
}
}
if(!ok)continue;
ans=min(ans,cost[i]);
}
cout<<ans<<endl;
}

浙公网安备 33010602011771号