安徽理工大学ACM挑战赛1月月赛-解题报告

T1:新年好

题目大意

小安和小理分别有A,B颗糖果,轮流吃掉一枚,先吃完者判负,给定先手问谁赢

sol

首先考虑两人糖果数不同的情况,显然谁的糖果多谁赢
然后考虑两人糖果数相同的情况,显然谁后手谁赢

参考代码

点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
	  int A,B,C; cin>>A>>B>>C;
	  if(A>B || (A==B && C))puts("An");
	  else puts("Li");
	  return 0;
}

T2:新概念鞭炮(easy version)

题目大意

给定一个01串,问最多可以分出多少个长度大于等于k的全1串且相互不交。

sol

首先在序列最后加入一个0,对结果没有影响
贪心地从前往后扫一遍01串,记录当前连续1的数量num,以及答案ans:

  • 当前值为1:连续的1的数量+1,即执行 ++num
  • 当前值为0:说明上一段一共有num个1,最多可以分出 \(ad=\lfloor \frac{num}{k}\rfloor\) 个长度大于等于 k 的全1串,将其贡献至答案:and+=ad,并将 num 清零

参考代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
int n,k; string s;
int main()
{
  cin>>k>>s; n=s.size();
  int cnt=0,ans=0;
  for(int i=0;i<n;i++){
    if(s[i]=='0')ans+=cnt/k,cnt=0;
    else ++cnt;
  }
  ans+=cnt/k;
  cout<<ans<<endl;
  return 0;
}

T3:新概念鞭炮(hard version)

题目大意

给定 \(k,n,m,t\),求解如下不定方程解的个数:

\[x_1 + x_2 + x_3 + ... + x_{t-1} = n\\ x_1,x_2 ... x_{t-1} \geq k\\ x_t = m >0 \]

sol

首先若 \(m\leq0\) 答案为 \(0\)
否则若 \(t=1, n=0\) 时答案为 \(1\)\(n>1\) 时答案为 \(0\)
否则若 \(x_i\) 没有大于等于 k 的限制,第一行式子是一个经典问题:相当于把 n 个小球分到 t-1 个本质不同的盒子里,求盒子允许为空的方案数。
考虑插板法,插入 \(t-2\) 个板子,问题转化为从 \(n+t-2\) 个球里选择 \(t-2\) 个球拿走,这时序列分成 \(t-1\) 份,跟上述问题的方案一一对应,所以该问题的方案数为一个简单的组合数:\(\binom{n+t-2}{t-2}\)
现在来处理 \(k\) 的限制,只需要强制每个盒子里先放入 \(k\) 个球,即令 \(n = n - k\times (t-1)\) 即可
逆元可以使用费马小定理求得:当模数 mod 为质数时,\(a^{mod-2}*a=1\),所以 \(a\) 的逆元为 \(a^{mod-2}\)(使用快速幂求解),根据组合数公式 \(\binom{n}{m} = \frac{n!}{m!(n-m)!}\),我们只需要递推计算出阶乘和阶乘的逆元即可。
当然,组合数也可以使用其递推公式在时限内求得,在此不再赘述。

参考代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
const int N=2002024,mod=998244353;
int n,m,k,t,fac[N],ivf[N];
int fpow(int x,int y=mod-2){ //快速幂计算逆元
	int res=1;
	for(;y;y>>=1,x=1ll*x*x%mod)
		if(y&1)res=1ll*res*x%mod;
	return res;
}
int C(int n,int m){ //计算组合数
	return 1ll*fac[n]*ivf[m]%mod*ivf[n-m]%mod;
}
int main()
{
	fac[0]=ivf[0]=1;
	for(int i=1;i<=2001000;i++){
		fac[i]=1ll*fac[i-1]*i%mod;
		ivf[i]=fpow(fac[i]);
	} // 计算阶乘及其逆元
	cin>>k>>n>>m>>t;
	if(!m)return puts("0"),0;
	else if(t==1)return puts(n?"0":"1"),0;
	--t;
	if(k*t>n)return puts("0"),0;
	int p=n-k*t; //处理 k 的限制
	int ans=1ll*C(p+t-1,t-1)*(t+1ll)%mod;
	cout<<ans<<endl;
	return 0;
}

T4:回家过年

题目大意

给定一张 \(n\) 个点 \(m\) 条边的带权无向图,给定 \(k\) 条从 \(1\) 号点到 \(1\) 号点外的其他顶点的带权双向边,假设加入了这条边,从从 \(1\) 号点到其他点的最短距离为 \(dis_i\),问最多可以少加入几条边,使得从 \(1\) 号点到其他点的最短距离仍然为 \(dis_i\),输出方案。

sol1

先将所有 \(k\) 条额外的道路加入无向图, 跑任意最短路算法, 得到 \(Dis_i\).

枚举 \(2^k\) 种情况, 每次判断每个点的最短路是否改变, 如果都没有, 则更新答案.

如果选择的最短路算法是 Dijkstra, 则复杂度为 \(O(2^km\log m)\). 可以拿到 \(40'\).

参考代码1

点击查看代码
Dij((1 << k) - 1);//传入的是状压的集合, 表示修建的道路
for (unsigned i(1); i <= n; ++i) N[i].Dis0 = N[i].Dis;
for (unsigned i(0); i < (1 << k) - 1; ++i) { //从小到大枚举集合
  Dij(i);
  char Flg = 0;
  for (unsigned j(1); j <= n; ++j) if(N[j].Dis0 < N[j].Dis) {
    Flg = 1;
    break;
  }
  if(!Flg) {//每个点的距离都是 Dis, 则找到方案
    for (unsigned j(0); j < k; ++j) if(!((1 << j) & i)) Ans.push_back(j + 1);
    break;
  }
}

sol2

发现如果某条新增的路 \((1, i)\) 存在或不存在, 使得若干点的距离发生改变, 那么 \(i\) 的距离一定是改变的距离之一. 且其它点距离改变量不大于 \(i\) 的距离改变量.

所以我们每次选择那个令 \(i\) 的最短路减少最多的道路新增, 直到无法新增为止, 最多进行 \(k\) 次最短路, 总复杂度 \(O(km\log m)\). 可以拿到 \(70\) 分.

参考代码2

点击查看代码
void Dij () {
  for (unsigned i(1); i <= n; ++i) N[i].Dis = 0x3f3f3f3f3f3f3f3f, N[i].Ava = 0;
  priority_queue<pair<unsigned long long, Node*> > Q;
  for (auto i:Newcomer) {
    Q.push({0x3f3f3f3f3f3f3f3f - New[i][1], N + New[i][0]});
    N[New[i][0]].Dis = min(N[New[i][0]].Dis, (unsigned long long)New[i][1]);
  }
  N[1].Dis = 0, N[1].Ava = 1;
  for (auto i:N[1].To) {
    i.first->Dis = min(i.first->Dis, (unsigned long long)i.second);
    Q.push({0x3f3f3f3f3f3f3f3f - i.second, i.first});
  }
  while (Q.size()) {
    Node *Cur = Q.top().second;
    Q.pop();
    if(Cur->Ava) continue;
    Cur->Ava = 1;
    for (auto i:Cur->To) {
      if(Cur->Dis + i.second < i.first->Dis) {
        i.first->Dis = Cur->Dis + i.second;
        Q.push({0x3f3f3f3f3f3f3f3f - i.first->Dis, i.first});
      }
    }
  }
} // Dij 前, 将 Newcomer 中记录的新增的边加入队列中

Dij();
for (unsigned i(1); i <= n; ++i) N[i].Dis0 = N[i].Dis;
Newcomer.clear();//第一次 Dij, 计算 Dis 值, 存入 Dis0

do {
  Dij();
  unsigned long long Mx(0);
  unsigned Pos;
  for (unsigned i(1); i <= k; ++i) if(N[New[i][0]].Dis - N[New[i][0]].Dis0 > Mx) {
    Mx = N[New[i][0]].Dis - N[New[i][0]].Dis0;
    Pos = i;// 加入该道路可以令路程改变更多
  }
  if(Mx) Newcomer.push_back(Pos);// 修建路程改变最多的道路
  else break;   // 已经满足 Dis 最小的要求
} while (1);

sol3

另外发现, 新增道路对于距离的减少无法叠加. 换种描述方式, 如果某个点的距离因为几条道路的修建减少了, 那么这几条道路中, 一定存在一条, 使得仅修建这一条也能达到同样的效果. 证明也很简单, 因为无论 \(Dis_i\) 所对应的路径是何种的, 由于无负环, 所以只会经过一次起点, 所以最多走一条新增道路.

定义 \(Dist\) 表示不新增道路的无向图上的最短路, 称 \(Dist_i \neq Dis_i\) 的点关键点. 对于某个关键点的 \(Dis\) 的不同方案, 第一步一定是通过某个新增道路 \((1, x)\), 走向一个关键点 \(x\). 这是因为如果不走新增道路, 那么该方案在 \(Dist\) 中也能得到, 而 \(i\) 的距离缩短, 是因为 \(x\) 的距离缩短了, 所以 \(x\) 也是一个关键点.

我们规定, 如果当且仅当走新增的 \((1, x)\) 道路才能在 \(Dis_x\) 到达 \(x\), 我们称 \(x\) 之为 "始源关键点".

我们希望证明: 始源关键点对应的道路就是必须修建的道路.

首先证明充分性. 只要修建了所有始源关键点的道路, 不仅始源关键点的 \(Dis\) 可以达成, 对于所有非始源的关键点, 它们任意达成 \(Dis\) 的路径就跑得通, 就可以达成 \(Dis\).

然后是必要性, 由始源关键点定义可知, 任何始源关键点的新增道路缺失都会造成该始源关键点无法达成 \(Dis\).

证毕后, 引出了一个结论, 修建道路最小的方案是确定且唯一的, 所以本题并不需要 spj.

为了找始源关键点, 引出另一个结论: 如果存在原有的边 \((i, x)\), 使得 \(Dis_i + (i, x) = Dis_x\), 则 \(x\) 不是始源关键点, 否则它就是始源关键点. 可以直接打 Tag 解决. (特别地, 根据定义 \(1\) 是始源关键点, 但是它不对应任何新建的边, 所以输出答案时需要忽略它)

参考代码3

点击查看代码
priority_queue<pair<unsigned long long, Node*> > Q;
for (unsigned i(1); i <= k; ++i) {
  A = RD(), B = RD();
  Q.push({0x3f3f3f3f3f3f3f3f - B, N + A});
  N[A].Num = i, N[A].Dis = min(N[A].Dis, (unsigned long long)B);
}// 将额外增加的边隐式加入图中 (直接丢进Dij的队列中)
N[1].Dis = 0, N[1].Ava = 1;
for (auto i:N[1].To) {
  i.first->Dis = min(i.first->Dis, (unsigned long long)i.second);
  Q.push({0x3f3f3f3f3f3f3f3f - i.second, i.first});
}// 跳过对起点的松弛, 直接将和起点相连的点加入队列
while (Q.size()) {
  Node *Cur = Q.top().second;
  Q.pop();
  if(Cur->Ava) continue;
  Cur->Ava = 1;
  for (auto i:Cur->To) {
    if(Cur->Dis + i.second < i.first->Dis) {
      i.first->Dis = Cur->Dis + i.second;
      Q.push({0x3f3f3f3f3f3f3f3f - i.first->Dis, i.first});
    }
  }
}// 未经任何改动的 Dij
for (unsigned i(1); i <= n; ++i)
  for (auto j:N[i].To)
    if(j.first->Dis == (N[i].Dis + j.second))
      j.first->Ava = 0; // 这里的 Ava 的意义变为: 是否为始源关键点
for (unsigned i(2); i <= n; ++i) if((!N[i].Ava) && (N[i].Num))
  Ans.push_back(N[i].Num);// 非始源关键点, 且有新增道路
printf("%lu\n", Ans.size());
sort(Ans.begin(), Ans.end());
for (auto i:Ans) printf("%u ", i);
putchar(0x0A);

T5:画画

题目大意

在笛卡尔坐标系里染色,每次染一个圆或者一个矩形,或者询问某个矩形中每一个点的颜色。

sol

一道需要细心和经验的模拟题。
我们发现染色的范围特别大,但是最后询问的范围很小,我们记录下每次染色,对于一次询问,我们枚举询问矩形中的每一个点,然后枚举之前的每一次染色,找到最后一次染上的颜色,然后输出答案即可。
会有很多坐标输出上的小细节,需要多加注意。

参考代码

点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N=2024;
#define int long long
struct node{int op,x,y,x2,y2,r; char col;}p[N];
int dis(int x,int y,int x2,int y2){ //计算两点间距离的平方
	return (x-x2)*(x-x2)+(y-y2)*(y-y2);
}
bool init(int x,int y,int x1,int y1,int x2,int y2){ //计算点是否在矩形内
	return (x>=x1 && x<=x2 && y>=y1 && y<=y2);
}
int read(){
	int x=0,f=1; char ch=getchar();
	while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0' && ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
signed main()
{
	int n=read();
	for(int T=1;T<=n;T++){
		string s; cin>>s;
		if(s[0]=='C'){p[T].op=1,p[T].x=read(),p[T].y=read(),p[T].r=read(); cin>>p[T].col;} //记录圆染色
		else if(s.size()>6){p[T].op=2,p[T].x=read(),p[T].y=read(),p[T].x2=read(),p[T].y2=read(); cin>>p[T].col;} //记录矩形染色
		else{
			int x1=read(),y1=read(),x2=read(),y2=read();
			for(int i=y2;i>=y1;i--,puts(""))
				for(int j=x1;j<=x2;j++){ //枚举询问矩形内的每一个点
					int fl=0;
					for(int k=T-1;k>=1;k--){
						if(!p[k].op)continue;
						if(p[k].op==1){
							if(dis(j,i,p[k].x,p[k].y)<=p[k].r*p[k].r){ //点是否在圆内
								putchar(p[k].col); fl=1; break;
							}
						}
						if(p[k].op==2){
							if(init(j,i,p[k].x,p[k].y,p[k].x2,p[k].y2)){ //点是否在矩形内
								putchar(p[k].col); fl=1; break;
							}
						}
					}
					if(!fl)putchar(46); //都不在,输出初始颜色
				}
		}
	}
	return 0;
}

总结

本次比赛的 T1 是有趣的小博弈题,T2 贪心,T3 计数,T4 最短路,T5 模拟,覆盖面广,不光考察选手的算法掌握,更考察了选手的数学功底和代码能力。
万丈高楼平地起,学习更多算法让我们兴奋,但也要多写代码增强代码能力,只有这样才能如履平地,越走越远。

posted @ 2025-02-02 22:11  lindongli2004  阅读(70)  评论(0)    收藏  举报