AtCoder 题目集1

AtCoder 题目集1

这是一个AT个人刷题总结的开始,感觉确实应该做一做这种总结,如果只是不断的刷题,感觉貌似也没有什么意思,还不如时常适当的回望一下过去的好题。希望能一直做下去吧。

update(22.12.14): AT赛后总结归为另外一栏,此处为过去AT题目的记录。

总结了一些比较有趣或者有思维的题(对于我这个菜鸡)。

编号(NO.) 题目 难度 类型
1 ARC060E 2154 倍增
2 ABC089D 1220 思维,前缀和
3 ABC087D 1452 思维,DFS
4 ABC277D 873 思维
5 ABC277E 1183 状态图,01BFS
6 ABC118D 1657 DP
7 ABC276E 1058 BFS,思维变形
8 ABC078D 1550 博弈论,思维
9 ABC013C 1692 思维
10 ABC143E 1695 思维,最短路

NO.1 ARC060E

倍增板题。

const int maxn=1e5+5;

int n,L,q;
int x[maxn],f[maxn][20],_2[20];

inline void Pre(){
	
	for(int i=1;i<n;++i){
		int l=i+1,r=n;
		while(l<r){
			int mid=(l+r+1)>>1;
			if(x[mid]-x[i]>L) r=mid-1;
			else l=mid;
		} f[i][0]=l;
	}
	
	_2[0]=1;
	for(int j=1;j<=18;++j){
		_2[j]=(_2[j-1]<<1);
		for(int i=1;i<=n;++i)
			f[i][j]=f[f[i][j-1]][j-1];
	}
} 

inline void solve(int x,int y){
	int res=0;
	for(int i=18;i>=0;--i){
		if(!f[x][i]||f[x][i]>y) continue;
		x=f[x][i];
		res+=_2[i];
	} if(x<y) ++res;
	cout<<res<<'\n';
}

int main(){
//  freopen("E.in","r",stdin);
//  freopen("E.out","w",stdout);
  IOS
  cin>>n;
  for(int i=1;i<=n;++i)
  	cin>>x[i];
  cin>>L>>q;
  
  Pre();
  
	int fo,to;
	while(q--){
  	cin>>fo>>to;
  	if(fo>to) swap(fo,to);
  	solve(fo,to);
	}
  return 0;
}

NO.2 ABC089D

思维题,但是没想出来。其实很显然需要一个可以快速统计答案的方法。一开始上来想了下倍增,但是显然麻烦了。可以用一种前缀的思想去做,因为变量值为 \(d\) ,所以可以预处理出从最小值到某个数的步数,然后用前缀思想就好了。

const int maxn=1e5+5;

int n,m,d,q;
int f[maxn],x[maxn],y[maxn];

int main(){
//  freopen("D.in","r",stdin);
//  freopen("D.out","w",stdout);
  IOS
  cin>>n>>m>>d; int p;
  for(int i=1;i<=n;++i)
  	for(int j=1;j<=m;++j)
  		cin>>p,x[p]=i,y[p]=j;
	
	for(int i=d+1;i<=n*m;++i)
		f[i]=f[i-d]+fabs(x[i]-x[i-d])+fabs(y[i]-y[i-d]);
	
	cin>>q;
	int l,r;
	while(q--){
		cin>>l>>r;
		cout<<f[r]-f[l]<<'\n';
	}
  return 0;
}

NO.3 ABC087D

这题应该很经典。

思路也比较好想到,对于一对关系,建边就好了,然后跑一边dfs,如果发现其中有一个关系不符合就直接返回false。可能有多个连通块所以要for一遍。

const int maxn=1e5+5;
const int maxm=2e5+5;

int n,m,cnt,head[maxn];
struct Edge{
	int to,nxt,val;
}edge[maxm<<1];

inline void Add(int fo,int to,int val){
	edge[++cnt]={to,head[fo],val};
	head[fo]=cnt;
}

int dis[maxn];
bool vis[maxn];
inline bool dfs(int x,int d){
	vis[x]=true,dis[x]=d;
	for(int i=head[x];i;i=edge[i].nxt){
		int &to=edge[i].to;
		if(vis[to]){
			if(dis[to]!=dis[x]+edge[i].val)
			return false;
		} else if(!dfs(to,d+edge[i].val))
			return false;
	}
	return true;
}

inline void solve(){
	for(int i=1;i<=n;++i)
		if(!vis[i])
			if(!dfs(i,0)){
				cout<<"No\n";
				return ;
			}
	cout<<"Yes\n";
}

int main(){
//  freopen("D.in","r",stdin);
//  freopen("D.out","w",stdout);
  IOS
  cin>>n>>m;
  int fo,to,val;
  for(int i=1;i<=m;++i){
  	cin>>fo>>to>>val;
  	Add(fo,to,val);
  	Add(to,fo,-val);
	}
	solve();
  return 0;
}

NO.4 ABC277D

这题...比赛时候一上来就想了一个tarjan缩点,结果很显然,打的很麻烦...又臭又长

const int maxn=2e5+5;
const int maxm=1e6+5;
 
int n,mod,a[maxn],cnt,head[maxn];
map <int,int> mp;
struct Edge{
	int fo,to,nxt;
}edge[maxm];
inline void Add(int fo,int to){
	edge[++cnt]={fo,to,head[fo]};
	head[fo]=cnt;
}
 
bool ins[maxn];
int dfn[maxn],low[maxn],s[maxn],top,belong[maxn],gp;
ll v[maxn];
inline void tj(int x){
	dfn[x]=low[x]=++cnt,s[++top]=x,ins[x]=true;
	for(int i=head[x];i;i=edge[i].nxt){
		int &to=edge[i].to;
		if(!dfn[to]){
			tj(to);
			low[x]=min(low[x],low[to]);
		} else if(ins[to]) low[x]=min(low[x],low[to]);
	}
	
	if(dfn[x]==low[x]){
		int c; ++gp;
		do{
			c=s[top--];
			ins[c]=false;
			belong[c]=gp;
			v[gp]+=a[c];
		} while(top&&c^x);
	}
}
 
int in[maxn];
ll dis[maxn];
inline ll topo(){
	queue<int>q; ll res=0;
	for(int i=1;i<=gp;++i)
		if(!in[i]) q.push(i),dis[i]=v[i];
	
	while(!q.empty()){
		int c=q.front(); q.pop();
		for(int i=head[c];i;i=edge[i].nxt){
			int &to=edge[i].to;
			dis[to]=Max(dis[to],dis[c]+v[to]);
			--in[to]; if(!in[to]) q.push(to);
		}
	}
	for(int i=1;i<=gp;++i) res=Max(res,dis[i]);
	return res;
}
 
ll tot;
inline void solve(){
	for(int i=1;i<=n;++i){
		if(!mp[a[i]])
			mp[a[i]]=i;
	}
	
	for(int i=1;i<=n;++i){
		if(a[i+1]==a[i])
			Add(i,i+1);
		if(mp[(1+a[i])%mod])
			Add(i,mp[(a[i]+1)%mod]);
	}
	
	int m=cnt;
	cnt=0;
	for(int i=1;i<=n;++i)
		if(!dfn[i]){
			top=0;
			tj(i);
		}
		
	cnt=0;
	memset(head,0,sizeof(head));
	for(int i=1;i<=m;++i){
		int x=belong[edge[i].fo],y=belong[edge[i].to];
		if(x^y){
			Add(x,y);
			in[y]+=1;
		}
	}
	cout<<tot-topo()<<'\n';
}
 
int main(){
  IOS
  cin>>n>>mod;
	for(int i=1;i<=n;++i)
		cin>>a[i],tot+=a[i];
	sort(a+1,a+1+n);
	solve();
  return 0;
}

跑了166ms。然而实际上,这题很好做。其实这道题麻烦(对我)主要还是在环那里,然而观察一下很容易(然而我没有)就能发现,其实循环的情况只可能是这样的形式:

\((m-2) \rightarrow (m-1) \rightarrow 0 \rightarrow 1...\)

所以很容易想到先排个序。那么因为给出的数都是小于 \(M\) 的,所以不用担心会有超出 \(M\) 的情况(草,忘了)。所以,开个两倍数组接在后面就完了啊!如果有循环,那就循环呗,最后再特判一下减出来会不会小于0就好了啊(因为如果整个都是个循环,可能会重复计算)!

const int maxn=2e5+5;

bool vis[maxn<<1];
int n,mod,a[maxn<<1];
ll tot;

inline void solve(){
	for(int i=1;i<=n;++i)
		a[i+n]=a[i];
	
	int pos=1; ll res=a[1],ans=0;
	a[n+n+1]=-1;
	while(pos<=n+n+1){
		if(a[pos+1]==a[pos]||a[pos+1]==(a[pos]+1)%mod)
			res+=a[pos+1];
		else
			ans=Max(ans,res),res=a[pos+1];
		++pos;
	}
	cout<<Max(0ll,tot-ans)<<'\n';
	
}

int main(){
//  freopen("D.in","r",stdin);
//  freopen("D.out","w",stdout);
  IOS
  cin>>n>>mod;
	for(int i=1;i<=n;++i)
		cin>>a[i],tot+=a[i];
	sort(a+1,a+1+n);
	solve();
  return 0;
}

NO.5 ABC277E

嗯,做这个题时读错题了,虽然好像然并卵,因为我已经摆了。

实际上这个题挺巧妙的(虽然只评了个1200)。

考虑对于给定的图,他按了开关后,所有能走的边都走不脱了,反之。那么再按一次,他的状态又回来了,所以一个图他只有两个状态。容易想到把另一个状态的图建出来,那么对于一个点,如果他想要另一个状态,他就必须按开关才行,所以只有开关点才能连通两个状态作为桥梁。

那么接下来就很容易了,发现是个01BFS,那就B呗。

感觉这题思路还是挺巧妙的...

const int maxn=2e5+5;

int n,m,k,cnt=1,head[maxn<<1],a[maxn<<1];
struct Edge{
	int fo,to,nxt,val;
}edge[maxn<<2];
inline void Add(int fo,int to,int val){
	edge[++cnt]={fo,to,head[fo],val};
	head[fo]=cnt;
}

deque <int> q;
int dis[maxn<<1];
bool vis[maxn<<1];
inline bool bfs_01(int pos){
	memset(dis,0x3f,sizeof(dis));
	q.push_front(pos);
	dis[1]=0;
	while(q.size()){
		int x=q.front(); q.pop_front();
		if(vis[x]) continue;
		vis[x]=true;
		for(int i=head[x];i;i=edge[i].nxt){
			int &to=edge[i].to;
			if(dis[to]>dis[x]+edge[i].val){
				dis[to]=edge[i].val+dis[x];
				if(edge[i].val) q.push_back(to);
				else q.push_front(to);
			}
		}
	}
	
	return vis[n]||vis[n<<1];
}

bool yon[maxn<<1];

inline void solve(){
	F(1,i,1,k){
		Add(a[i],n+a[i],0);
		Add(n+a[i],a[i],0);
	}
	
	if(bfs_01(1))
		cout<<Min(dis[n],dis[n<<1])<<'\n';
	else
		cout<<-1<<'\n';
}

int main(){
//  freopen("E.in","r",stdin);
//  freopen("E.out","w",stdout);
  IOS
  cin>>n>>m>>k;
  int fo,to,val;
	for(int i=1;i<=m;++i){
  	cin>>fo>>to>>val;
  	if(val){
  		Add(fo,to,val);
  		Add(to,fo,val);
		} else{
	  	Add(fo+n,to+n,val^1);
	  	Add(to+n,fo+n,val^1);
		}
	}
	for(int i=1;i<=k;++i)	cin>>a[i];
	solve();
  return 0;
}

NO.6 ABC118D

题意翻译:
有n根火柴,m种数字,数字1,2,3,4,5,6,7,8,9分别需要2,5,5,4,5,6,3,7,6根火柴,要求n根火柴全部都用完且拼成的数字最大,输出这个数字。

一道很好的dp思维题。

然而我又聪明地把它看成了一道贪心,然后扣了半天都没扣出个东西来...

实际上还是比较容易推出dp方程的,很显然,dp需要记录的应该是所能形成的最大数字,那么用string就好了,然后加个判断函数。

发现 \(n \leqslant 10^4\) ,那么dp思路就出来了:\(f_i\) 表示用了 \(i\) 根火柴所能组成的最大数字,显然,最后加上的数字最多就只有9个,那么暴力递推一下就好了。

当然,需要注意的是,它要求刚好用完 \(n\) 根火柴,所以还需要考虑初始化,所以我把 \(f_i\) 一开始都设为了 “-”,\(f_0\) 为空。

const int N=10010;

struct Node{int v,c;}a[10];
int n,m,mp[]={0,2,5,5,4,5,6,3,7,6};
string f[N];

inline bool check(string A,string B){
	if(A=="-") return true;
	if(A.length()<B.length()) return true;
	else if(A.length()>B.length()) return false;
	else return A<B;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin>>n>>m;
	for(int i=1,x;i<=m;++i){
		cin>>x;
		a[i]={x,mp[x]};
	}
	
	for(int i=1;i<=n;++i)
		f[i]="-";
	f[0]="";
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j)
			if(a[j].c<=i) if(f[i-a[j].c]!="-")
				if(check(f[i],f[i-a[j].c]+char(a[j].v+'0')))
					f[i]=f[i-a[j].c]+char(a[j].v+'0');
	}
//	for(int i=1;i<=n;++i)
//		cout<<i<<':'<<f[i]<<'\n';
	cout<<f[n]<<'\n';
	return 0;
}

NO.7 ABC276E

是道很有趣但是又有点恼人的题...

初看就很简单,但是感觉好像也不是很好判断...

然后想了半天,一直在bfs和dfs之间纠结,最后发现了一种比较好维护的bfs方法。

因为其实如果能形成回路,那么一定存在一个一进一出,所以可以考虑先把出发点四周的四个临近的点标上不同的号,然后进行bfs,如果发现有两个不同的标号有相交,则说明形成了回路。

const int N=1000010;

int n,m,sx,sy,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
char a[N];

inline int trans(int x,int y)
{return (x<1||x>n||y<1||y>m)?1e9:m*(x-1)+y;}

struct Node{int x,y,type;};
queue<Node> q;
inline bool check(){
  for(int i=0;i<4;++i){
    int x=trans(sx+dx[i],sy+dy[i]);
    if(x>0&&x<=n*m&&a[x]!='#') q.push({sx+dx[i],sy+dy[i],i+1});
  } a[trans(sx,sy)]='#';

  while(!q.empty()){
    Node c=q.front(); q.pop();
    a[trans(c.x,c.y)]=c.type;
    for(int i=0;i<4;++i){
      int to=trans(c.x+dx[i],c.y+dy[i]);
      if(to<1||to>n*m) continue;
      if(a[to]!='#'&&a[to]!='.'&&a[to]!=c.type) return true;
      if(a[to]=='.'){
        q.push({c.x+dx[i],c.y+dy[i],c.type});
        a[to]=c.type;
      }
    }
  }
  return false;
}

int main(){
  ios::sync_with_stdio(false);
  cin.tie(NULL); cout.tie(NULL);
  
  cin>>n>>m;
  for(int i=1;i<=n;++i)
    for(int j=1;j<=m;++j){
      cin>>a[trans(i,j)];
      if(a[trans(i,j)]=='S')
        sx=i,sy=j;
    }
  if(check()) cout<<"Yes\n";
  else cout<<"No\n";
  return 0;
}

NO.8 ABC078D

感觉很牛马的证明题。

首先,如果只有一张牌,那么显然答案是 \(abs(a_n-y)\)

如果有两张牌,那么答案就是 \(max(abs(a_n-a_{n-1}),abs(a_n-y))\)

现在看大于两张牌的情况,

首先,显然先手可以取 \(n-1\) 或者 \(n\) 张牌,使得最后的答案和 \(n=2\) 时的一样,那么取完后剩余排数大于1张时呢?结论就是,根本没有必要,所以实际上 \(n>2\) 的情况和 \(n=2\) 的情况是一模一样的。

为什么捏?思考一下,如果你把主动权交给了后手,那么此时还剩下的两张或者更多的牌中,显然后手可以取到最后只剩下一张牌,那么也就是说最后的答案可以由后手决定为 \(a_n-a_{n-1}\) ,然而实际上第一轮选择时先手就已经可以达到这个答案了,所以如果你先手不取 \(n-1\) 或者 \(n\) 张牌,那么就相当于给了后手更多的减小答案的可能性,其中后手可以选择先手就能决定的答案,或者选择更小的答案。即不可能有比先手直接完或者剩下一张更优的答案了。

所以先手还不如一开始就决定好,因此,\(n\geqslant2\) 时,\(ans=max(abs(a_n-a_{n-1}),abs(a_n-y))\)

感觉其实这道题挺神奇的,它根本就没有讨论中间拿取的过程,而直接得出了最后的结论,因为中间的过程根本就没有必要。

const int N=2005;

int n,a[N],fx,fy;

int main(){
  ios::sync_with_stdio(false);
  cin.tie(NULL); cout.tie(NULL);

  cin>>n>>fx>>fy;
  for(int i=1;i<=n;++i)
    cin>>a[i];
  
  if(n==1||a[n]==a[n-1]) return cout<<abs(a[n]-fy)<<'\n',0;
  else cout<<max(abs(a[n]-a[n-1]),abs(a[n]-fy))<<'\n';

  return 0;
}

NO.9 ABC013C

这题满分 \(101\) 确实很有意思...

首先思考,先吃一定比后吃优,所以不妨直接考虑吃多少天正常餐和简单餐。

假如吃 \(x\) 天正常餐,\(y\) 天简单餐,那么应该满足 \(h+bx+dy-e(n-x-y)>0\)

显然是个一次函数,所以对于每个 \(x \in [0,n]\) 都求出最小 \(y\) 即可。

signed main(){
  //freopen();
  //freopen();
  IOS
  //int T;
  
  cin>>n>>h>>a>>b>>c>>d>>e;
  
  ll ans=llinf;
  for(ll x=0;x<=n;++x){
    ll A=(e+b),B=(e+d),C=(h-e*n);
    ll l=0,r=n-x;
    while(l<r){
      ll mid=(l+r)>>1;
      if(A*x+B*mid+C>0) r=mid;
      else l=mid+1;
    } ans=Min(ans,x*a+l*c);
    if(x*a+l*c==60) cout<<x<<' '<<l<<'\n';
  }
  cout<<ans<<'\n';
  return 0;
}

NO.10 ABC143E

真是道好题。

首先看到 \(n \leqslant 300\) 就先跑一遍 \(Floyd\) ,注意有些边要删。然后就需要思考了。

不难想到只有间距小于 \(L\) 的两点才能一步到达,所以再建个新图,其中边连着原图中间距小于 \(L\) 的两点,且边权为 \(1\) 。然后再跑个最短路 \(bfs\) 就好了。

const int N=305;

int n,m,cnt,head[N];
struct Edge{int to,nxt;}edge[N*N];
inline void Add(int fo,int to){
  edge[++cnt]={to,head[fo]};
  head[fo]=cnt;
} ll l,dis[N][N];

void bfs(int ori){
  queue<int> d;
  d.push(ori);
  dis[ori][ori]=0;
  while(d.size()){
    int pos=d.front(); d.pop();
    for(int i=head[pos];i;i=edge[i].nxt){
      int& to=edge[i].to;
      if(dis[ori][to]>dis[ori][pos]+1){
        dis[ori][to]=dis[ori][pos]+1;
        d.push(to);
      }
    }
  }
}

signed main(){
  //freopen();
  //freopen();
  IOS
  //int T;
  cin>>n>>m>>l;
  vector<pair<int,pair<int,ll>>> v;
  for(int i=1;i<=m;++i){
    int fo,to; ll val;
    cin>>fo>>to>>val;
    if(val>l) continue;
    v.push_back(make_pair(fo,make_pair(to,val)));
    v.push_back(make_pair(to,make_pair(fo,val)));
  }

  memset(dis,0x3f3f,sizeof(dis));
  for(int i=1;i<=n;++i) dis[i][i]=0;
  for(auto t:v) dis[t.first][t.second.first]=t.second.second;

  for(int i=1;i<=n;++i)
    for(int j=1;j<=n;++j)
      for(int k=1;k<=n;++k)
        dis[j][k]=Min(dis[j][k],dis[j][i]+dis[i][k]);
  
  for(int i=1;i<=n;++i)
    for(int j=1;j<=n;++j)
      if(i^j&&dis[i][j]<=l)
        Add(i,j);
  
  memset(dis,0x3f,sizeof(dis));
  for(int i=1;i<=n;++i) bfs(i);

  int q;
  cin>>q;
  while(q--){
    int s,t;
    cin>>s>>t;
    if(dis[s][t]>114514){
      cout<<-1<<'\n';
      continue;
    } else cout<<dis[s][t]-1<<'\n';
  }
  
  return 0;
}

23.8.4 EOF

posted @ 2023-08-20 09:28  ComplexityMFC  阅读(106)  评论(0编辑  收藏  举报