ACM:图论+数据结构杂题总结
T1:
题目描述:(出处:Atcoder Regular Contest 067 Yakiniku Restaurants)
一条街上有N家烧烤餐馆。餐厅从西到东编号为1到N,I餐厅和i+1餐厅之间的距离为Ai。
乔伊辛诺有M张票,编号从1到M。每个烧烤餐厅都提供烧烤餐来交换这些票。餐厅i提供美味的Bi,j餐,以换取j票。每张票只能使用一次,但是餐厅可以使用任意数量的票。
乔伊辛诺想从她选择的一家餐厅开始,然后反复去另一家烧烤餐厅,并在她目前所在的餐厅使用未用过的票,来享用M份烧烤餐。
她的最终幸福是通过以下公式来计算的:(所吃食物的总美味)-(旅行的总距离)。
找到她最大可能的最终幸福。
数据范围
- 所有输入值都是整数。
- 2≤N≤5×103
- 1≤M≤200
- 1≤Ai≤109
- 1≤Bi,j≤109
输入
标准输入以以下格式给出输入:
N M
A1 A2 … AN−1
B1,1 B1,2 … B1,M
B2,1 B2,2 … B2,M
:
BN,1 BN,2 … BN,M
输出:
乔伊辛诺最终可能获得的最大幸福。
input1
3 4
1 4
2 2 5 1
1 3 3 2
2 2 5 1
output1
11
【解析】:考虑暴力枚举区间(必然从一个端点走向另一个端点),对于每一个票 j j j选择价值最大的那个店购买,时间复杂度为(O(n2 m))
这样就得出一个显然的贪心结论:在一个区间里,对于票 j j j,一定会选择最大 B [ i , j ] B[i,j] B[i,j]( i i i在区间中)。那么不妨来考虑每一个 B [ i , j ] B[i,j] B[i,j]能够带来的贡献。
设 f [ i ] [ j ] f[i][j] f[i][j]表示在区间 [ i , j ] [i,j] [i,j]花完 m m m张票的最大价值,很显然路程代价为区间长度。
那么处理出一定在i店买j票的最大区间为 [ a , b ] [a,b] [a,b],即 a < = i < = b a<=i<=b a<=i<=b且 a − 1 a-1 a−1为 B [ a − 1 , j ] > B [ i , j ] B[a-1,j]>B[i,j] B[a−1,j]>B[i,j]的最大位置, b + 1 b+1 b+1为 B [ b + 1 , j ] > B [ i , j ] B[b+1,j]>B[i,j] B[b+1,j]>B[i,j]的最小位置,这个东西可以用单调栈维护出来 a , b a,b a,b。那么一定在i点买j票的区间就有左端点属于 [ a , i ] [a,i] [a,i],右端点属于 [ i , b ] [i,b] [i,b],然后在对应的每一个f数组上累计贡献,发现这其实一个区域矩形累加,利用二维差分来实现。【把 f f f数组看成一个矩阵,横坐标为左端点,纵坐标为右端点,那么对于的所有区间就是以 [ a , i ] [a,i] [a,i]为左下角,以 [ i , b ] [i,b] [i,b]为右上角的大矩形(针对第一象限)】
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=5e3+10;
int stk[N],n,m,l[N][210],r[N][210],a[N][205],top=0;
//l[i][j]--左边第一个>B[i,j]的位置 r[i][j]--右边以第一个>B[i,j]的位置
ll dis[N],f[N][N],ans=0;
inline void add(int r1,int c1,int r2,int c2,ll v)//二维差分trick---划重点
{
f[r1][c1]+=v; f[r1][c2+1]-=v; f[r2+1][c1]-=v; f[r2+1][c2+1]+=v;
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=2;i<=n;i++) scanf("%d",&dis[i]),dis[i]+=dis[i-1];//处理距离
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
top=0;
for(int i=1;i<=m;i++)//第i张票
{
top=0;
for(int j=1;j<=n;j++)
{
while(top&&a[stk[top]][i]<a[j][i]) top--;
l[j][i]=(top?stk[top]:0);
stk[++top]=j;
}
top=0;
for(int j=n;j>=1;j--)
{
while(top&&a[stk[top]][i]<a[j][i]) top--;
r[j][i]=(top?stk[top]:n+1);
stk[++top]=j;
}
for(int j=1;j<=n;j++)
add(l[j][i]+1,j,j,r[j][i]-1,a[j][i]);
}
for(int i=1;i<=n;i++)//前缀和:将差分还原成原矩阵
for(int j=1;j<=n;j++)
{
f[i][j]+=f[i-1][j]+f[i][j-1]-f[i-1][j-1];
if(j>=i) ans=max(ans,f[i][j]-(dis[j]-dis[i]));
}
printf("%lld\n",ans);
return 0;
}
T2:
题目描述:(出处:Atcoder Regular Contest 080 Young Maids)
给定一个n(n为偶数)的p排列,有一个空队列q,每次从p中删除两个相邻的数插入q的队头,进行n/2次以后p排列为空了,问q队列最终的排列的最小字典序是什么?
数据范围: 1 < = n < = 2 e 5 1<=n<=2e5 1<=n<=2e5
【解析】:倒着贪心,最后我们放在最前面的,就是最小的奇数位的数加上最小的偶数位的数。
(奇数位前面保证完全匹配,偶数位后面保证完全匹配,不会有剩余)
然后我们放完这段之后,我们发现我们把原来的区间就会砍为三段,然后再在每一段找到最小的两个数即可。不停的贪心下去就好了。
奇偶数位的最小值可以用ST来维护,用一个优先堆来存放两个二元组:{{在选择区间下的最小奇数位,在选择区间下的最小偶数位(相对选择区间)},{选择区间的左端点,选择区间的右端点}}(既然我们已经采用了奇偶性选择数,就能够保证当前选择区间以外的区间能都完全匹配,那么只需要考虑当前区间左端点的奇偶【如:一开始 [ 1 , n ] [1,n] [1,n]区间一开始找最小奇数位(此时选择区间的左端点为奇数),再找最小偶数位】)
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=2e5+7;
int n,p[N],f[2][N][20],Log[N],pos[N];
inline int ask(int opt,int l,int r)
{
int k=Log[r-l+1];
return min(f[opt][l][k],f[opt][r-(1<<k)+1][k]);
}
inline pair<int,int> cal(int l,int r)
{
int x,y;
x=ask(l&1,l,r);//相对
y=ask((l-1)&1,pos[x]+1,r);
return make_pair(-x,-y);
}
priority_queue<pair<pair<int,int>,pair<int,int > > > q;
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&p[i]);
f[i&1][i][0]=p[i];
f[(i-1)&1][i][0]=1e9+7;
pos[p[i]]=i;
}
Log[1]=0;
for(int i=2;i<=N-7;i++)
Log[i]=Log[i>>1]+1;
for(int k=0;k<=1;k++)//奇or偶
for(int j=1;j<=19;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
f[k][i][j]=min(f[k][i][j-1],f[k][i+(1<<(j-1))][j-1]);
q.push({{cal(1,n)},{1,n}});
while(q.size())
{
pair<pair<int,int>,pair<int,int> > tmp=q.top(); q.pop(); //cout<<tmp.second.first<<endl;
int x=tmp.first.first,y=tmp.first.second;
printf("%d %d ",-x,-y);
x=pos[-x],y=pos[-y];
int l=tmp.second.first,r=tmp.second.second;
//cout<<l<<" "<<r<<" "<<x<<" "<<y<<endl;
if(l+1<x) q.push({{cal(l,x-1)},{l,x-1}});
if(x+1<y-1) q.push({{cal(x+1,y-1)},{x+1,y-1}});
if(y+1<r) q.push({{cal(y+1,r)},{y+1,r}});
}
return 0;
}
T3:再见!
(出处:codeforces gym 101955 E The Kouga Ninja Scrolls)
T4:
题面描述(出处:Atcoder Grand Contest 012 Splatter Painting)
Squid 喜欢在图中为一些顶点染色(毕竟是鱿鱼…)
现在有一张由N个顶点和M条边组成的简单无向图,其
中顶点编号为1到N。我们用数字来编号各种颜色。
一开始,所有的顶点都会被染成颜色0。第i条双向边
连接着两个端点 a [ i ] a[i] a[i]和 b [ i ] b[i] b[i]。每条边的长度都是单位1。
Squid会在这张图上进行Q次操作。其中对于第i次操作,
他会将与顶点 v [ i ] v[i] v[i]相距 d [ i ] d[ i ] d[i] 以内(包括 v [ i ] v[i] v[i])的所
有点重新染色成颜色 c [ i ] c[i] c[i]
请问Q次操作后,每个顶点的颜色各是什么。
数据范围
1≤N,M,Q≤105
1≤ai,bi,vi≤N
ai≠bi
0≤di≤10
1≤ci≤105
di 和 ci 是整数
没有自环和重边
Input
输入来自标准输入,格式如下:
N M
a1 b1
:
aM bM
Q
v1 d1 c1
:
vQ dQ cQ
Output
答案一共 N 行。在第 i 行中,在 Q 操作之后输出顶点 i 的颜色。
【解析】:由于节点会被最后染的颜色所覆盖,所以我们考虑倒着染色,已经染过色的节点就不用再考虑了。然后记忆化搜索,设 f [ i ] [ j ] f[i][j] f[i][j]表示距离i节点距离为j的颜色。
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1e5+7;
int tot,n,m,q,v[N],d[N],c[N],first[N];
int f[N][11];//距离i点为j的点的颜色
struct fuk
{
int x,next;
}a[N<<2];
inline void add(int x,int to)
{
tot++;
a[tot].next=first[x]; a[tot].x=to; first[x]=tot;
}
void dfs(int x,int d,int col)
{
if(d==-1) return;
if(f[x][d]) return;
f[x][d]=col;
for(int i=first[x];i;i=a[i].next)
{
int y=a[i].x;
dfs(y,d-1,col);
}
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y; scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=1;i<=n;i++) add(i,i);//这一步操作至关重要,保证可以在d距离之内全部染色
scanf("%d",&q);
for(int i=1;i<=q;i++) scanf("%d%d%d",&v[i],&d[i],&c[i]);
for(int i=q;i>=1;i--)
dfs(v[i],d[i],c[i]);
for(int i=1;i<=n;i++)
printf("%d\n",f[i][0]);
return 0;
}
T5:
题目描述:(出处:codeforces 567 E President and Roads)
给定一个n个点,m条边的有向图,询问每一条的状态
1.一定在起点s到终点t的最短路上:输出“YES”
2.通过减少x(x<边权)使得一定在最短路上:输出"CAN x"
3.前面2则都不满足:输出"NO"
2<=n<=1e5,1<=m<=1e5,1<=s,t<=n,1<=边权<=1e6;
【解析】:
一个点x在最短路上的条件: d i s [ s ] [ x ] dis[s][x] dis[s][x]+ d i s [ x ] [ t ] dis[x][t] dis[x][t]= d i s [ s ] [ t ] dis[s][t] dis[s][t]
一个点x一定在最短路上的条件: a n s [ s ] [ x ] ans[s][x] ans[s][x]* a n s [ x ] [ t ] ans[x][t] ans[x][t]= a n s [ s ] [ t ] ans[s][t] ans[s][t](其中 a n s [ i ] [ j ] ans[i][j] ans[i][j]表示i到j的最短路的方案数)
d i s [ s ] [ x ] dis[s][x] dis[s][x]和 a n s [ s ] [ x ] ans[s][x] ans[s][x]我们可以一遍堆优化dj的时候顺便求出。
明显 d i s [ x ] [ t ] dis[x][t] dis[x][t]和 a n s [ x ] [ t ] ans[x][t] ans[x][t]我们都可以通过建反图同理求出。
然后就是分类讨论即可。
坑点:方案数很大可能会爆ll,所以要模一个大质数。(但不知道为什么不能是1e9+7)…然后dis数组权值一开始赋值大一点…
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1e5+7,mod=998244353;
const ll INF=1e15;
inline ll read()
{
ll sum=0; char ch;
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') sum=(sum<<1)+(sum<<3)+ch-'0',ch=getchar();
return sum;
}
inline ll Add(ll a,ll b){return (ll)a+b>=mod?a+b-mod:a+b;}
ll n,m,s,e,tot1,tot2,first1[N],first2[N],vis[N];
ll dis[2][N],ans[N],sna[N];//0--ZHENG 1--FAN
struct fuk
{
ll x,next;
ll v;
}a[N<<1],b[N<<1];
struct fuc
{
ll u,v;
ll w;
}c[N];
inline void add1(ll x,ll to,ll v)
{
tot1++;
a[tot1].x=to; a[tot1].next=first1[x]; first1[x]=tot1; a[tot1].v=v;
}
inline void add2(ll x,ll to,ll v)
{
tot2++;
b[tot2].x=to; b[tot2].next=first2[x]; first2[x]=tot2; b[tot2].v=v;
}
inline void DJ1()
{
priority_queue<pair<ll,int> >q;
while(q.size()) q.pop();
for(int i=1;i<=n;i++) dis[0][i]=INF,vis[i]=0;
q.push(make_pair(0,s)); dis[0][s]=0,ans[s]=1;
while(q.size())
{
int x=q.top().second; q.pop();
if(vis[x]) continue; vis[x]=1;
for(int i=first1[x];i;i=a[i].next)
{
int y=a[i].x;
if(dis[0][y]>dis[0][x]+a[i].v)
{
dis[0][y]=dis[0][x]+a[i].v;
q.push(make_pair(-dis[0][y],y));
ans[y]=ans[x];
}
else if(dis[0][y]==dis[0][x]+a[i].v)
ans[y]=Add(ans[y],ans[x]);
}
}
}
inline void DJ2()
{
priority_queue<pair<ll,int> >q;
while(q.size()) q.pop();
for(int i=1;i<=n;i++) dis[1][i]=INF,vis[i]=0;
q.push(make_pair(0,e)); dis[1][e]=0,sna[e]=1;
while(q.size())
{
int x=q.top().second; q.pop();
if(vis[x]) continue; vis[x]=1;
for(int i=first2[x];i;i=b[i].next)
{
int y=b[i].x;
if(dis[1][y]>dis[1][x]+b[i].v)
{
dis[1][y]=dis[1][x]+b[i].v;
q.push(make_pair(-dis[1][y],y));
sna[y]=sna[x];
}
else if(dis[1][y]==dis[1][x]+b[i].v)
sna[y]=Add(sna[y],sna[x]);
}
}
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n=read(),m=read(); s=read(); e=read();
for(int i=1;i<=m;i++)
{
ll x=read(),y=read(),z=read();
add1(x,y,z); add2(y,x,z);
c[i].u=x,c[i].v=y,c[i].w=z;
}
DJ1(); DJ2();
for(int i=1;i<=m;i++)
{
ll x=c[i].u,y=c[i].v;
if(dis[0][x]+dis[1][y]+c[i].w==dis[0][e])
{
if((ll)ans[x]*sna[y]%mod==ans[e])
{
puts("YES");
}
else if(c[i].w>1) puts("CAN 1");
else puts("NO");
}
else
if(dis[0][x]+dis[1][y]-dis[0][e]+1>=0)
{
puts("NO");
}
else printf("CAN %lld\n",dis[0][x]+dis[1][y]+c[i].w-dis[0][e]+1);
}
return 0;
}
//吐槽:老刘总是说这样重复的可以写成一个函数,但是我就是懒233
T6:
题目描述:(出处:codeforces 1205 B Shortest Cycle)
已知n个整数a1 a2…an。考虑n个节点上的图,其中i, j (i≠j)相互连接仅当ai and aj≠0时,其中and表示按位与运算。
求出这个图中最短环的长度(>=3),如果没有环,输出-1。
1 < = n < = 1 e 5 , 0 < = a i < = 1 e 18 1<=n<=1e5,0<=ai<=1e18 1<=n<=1e5,0<=ai<=1e18(接近2^64)
【解析】:根据抽屉原理,0不算在内,数量如果>2*64,那么一定存在一个二进制位有三个数都是1,即存在最小环=3.
否则,你会发现数量很少,直接各种求最小环。
(这里使用的是floyd判环(划重点))
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+7;
const int INF=1e5+7;
int n,tot=0;
ll res[N];
int a[201][201],f[201][201];
int ans=N;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
ll x; scanf("%lld",&x);
if(x) res[++tot]=x;
}
if(tot>2*64)
{
puts("3");
return 0;
}
n=tot;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
if((res[i]&res[j])!=0)
{
a[i][j]=a[j][i]=f[i][j]=f[j][i]=1;
}
else a[i][j]=a[j][i]=f[i][j]=f[j][i]=INF;
for(int k=1;k<=n;k++)
{
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++)
ans=min(ans,f[i][j]+a[i][k]+a[k][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
printf("%d\n",ans==INF?-1:ans);
}
T7:
题面描述:(出处:codeforces gym 101257 F ISlands II)
给出一个n*m的地图,上面相同数字的代表一个国家,问对于每个国家有多少个国家在它内部(即被包围)。例如样例,1包围2,2包围3,所以1包围2和3,2包围3。
(1<=n,m<=1000)
样例:
1 1 1 1 1 1
1 2 2 2 2 1
1 2 4 3 2 1
1 2 3 3 2 1
1 2 2 2 2 1
1 1 1 1 1 1
【解析】:对于一个国家,将和它相邻的国家连边,最后形成一个图。可以发现图中的割点一定包围了一些国家。如图中的2号点,但是无法确定包围了1还是包围了3,4,所以我们再在最外面加一圈0,从0开始dfs,发现一个割点,那么割点一定包围了不在0一端的国家。
0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 0
0 1 2 2 2 2 1 0
0 1 2 4 3 2 1 0
0 1 2 3 3 2 1 0
0 1 2 2 2 2 1 0
0 1 1 1 1 1 1 0
0 0 0 0 0 0 0 0
图变成这样了。
于是就可以使用tarjan来找割点,割点就包围了一些国家。一开始dfs一遍,维护一个size代表子树的大小。然后如果该点是割点,就可以加上其子树的大小。
PS:不用考虑0号点是否是割点,数组开大,国家数不止1000。
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1000005;
int tot,vis[N],m[1005][1005],n,M,size[N],dfn[N],low[N],cnt=0,ans[N],first[N];
struct fuk
{
int x,next;
}a[N<<3];//8倍了解一下
inline void add(int x,int to)
{
tot++;
a[tot].x=to; a[tot].next=first[x]; first[x]=tot;
}
void dfs(int x)
{
size[x]=1; vis[x]=1;
for(int i=first[x];i;i=a[i].next)
{
int y=a[i].x;
if(vis[y]) continue;
dfs(y);
size[x]+=size[y];
}
}
void tarjan(int x)
{
dfn[x]=low[x]=++cnt;
for(int i=first[x];i;i=a[i].next)
{
int y=a[i].x;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(dfn[x]<=low[y]) ans[x]+=size[y];
}
else low[x]=min(low[x],dfn[y]);
}
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%d%d",&n,&M);
for(int i=1;i<=n;i++)
for(int j=1;j<=M;j++)
scanf("%d",&m[i][j]);
for(int i=0;i<=n;i++)//包0
for(int j=0;j<=M;j++)
{
if(m[i][j]!=m[i][j+1]) add(m[i][j],m[i][j+1]),add(m[i][j+1],m[i][j]);
if(m[i][j]!=m[i+1][j]) add(m[i][j],m[i+1][j]),add(m[i+1][j],m[i][j]);
}
dfs(0); tarjan(0);
for(int i=1;i<=cnt-1;i++)
printf("%d ",ans[i]);
return 0;
}
总结反思:题做的还是不够多,模型的建立还是不够熟练,还是要多多思考。
CSP ++RP