2018-2019 ACM-ICPC, Asia Nanjing Regional Contest

  1. Adrien and Austin 博弈、思维

大意:给定 n 个石子,下标1-n。两人轮流取石子。最少1个最多k个,并且取的石子下标连续,不能操作的判输

思路:当 k!=1 时,显然先手可以去中间一个或两个使得石子分成对称的两段,然后一直做和后手一样的操作就行了。先手必胜。

当 k=1 时每次只能取一个,分就讨论就行了。注意这题比较坑的一个地方是n是可以等于0的。

  1. Country Meow 模拟退火

大意:三维里有n个点,找一个最小的球将所有点覆盖。

思路:方法1:解析几何最优解问题,直接上模拟退火,效果还挺好。一发就A.

代码如下:

#include <bits/stdc++.h>
using namespace std;
struct Point{
	double x,y,z;
}a[110];
double ans=0x3f3f3f3f;
int n;
double cale(Point A,Point B)
{
	double dx=A.x-B.x;
	double dy=A.y-B.y;
	double dz=A.z-B.z;
	return sqrt(dx*dx+dy*dy+dz*dz);
}
void sa()
{
	Point pos={0,0,0};
	double t=100000;
	while(t>1e-15)
	{
		Point np;
		np.x=pos.x+(rand()*2-RAND_MAX)*t;
		np.y=pos.y+(rand()*2-RAND_MAX)*t;
		np.z=pos.z+(rand()*2-RAND_MAX)*t;
		double mx=0;
		for(int i=1;i<=n;i++)mx=max(mx,cale(a[i],np));
		
		double de=mx-ans;
		if(de<0)
		{
			ans=mx;
			pos=np;
		}
		else if(exp(-de/t)*RAND_MAX>rand())pos=np;
		t*=0.997;
		
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		double x,y,z;
		cin>>x>>y>>z;
		a[i]={x,y,z};
	}
	sa();
	printf("%.15f\n",ans);
	return 0;
}

方法2:x,y,z 三个坐标三分套三分套三分就好了,为了好写我们直接dfs套三分,代码如下:

#include <bits/stdc++.h>
using namespace std;
int n;
struct point{
	double x,y,z;
}a[110];
double p[3];
double cale()
{
	double ans=0;
	for(int i=1;i<=n;i++)
	{
		double dx=a[i].x-p[0],dy=a[i].y-p[1],dz=a[i].z-p[2];
		ans=max(ans,sqrt(dx*dx+dy*dy+dz*dz));
	}
	return ans;
}
double solve(int x)
{
	if(x==3)return cale();
	double l=-1e5,r=1e5,ans=0x3f3f3f3f;
	while(r-l>1e-5)
	{
		double lmid=l+(r-l)/3,rmid=r-(r-l)/3;
		p[x]=lmid;
		double ansl=solve(x+1);
		p[x]=rmid;
		double ansr=solve(x+1);
		if(ansl<=ansr)r=rmid,ans=ansl;
		else l=lmid,ans=ansr;
	}
	return ans;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		double x,y,z;
		cin>>x>>y>>z;
		a[i]={x,y,z};
	}
	printf("%.15f\n",solve(0));
	return 0;
}

总结:模拟退火比写三分快了10倍不止,随机大法好哇!!而且还不容易写错。当然如果实在脸黑还是得老老实实写三分。

  1. Country Meow 思维

大意:给定两个01字符串 s,t 以及k。每次操作可以把 k个连续相同的1或0变成0或1。问是否能通过若干次操作将字符串 s 变成t。

思路:对于这类题,我们不能想着怎么去模拟,而是找到操作的本质。 对于连续的 k 个相同的字符 k=4, xxxxy 显然我们可以 变成 yyyyy

进而变成 yxxxx 也就是相当于往后移动直至移动到末尾,相当于把这连续k个给去掉了。也就是说对于每连续的k个我们都可以去掉,然后我们比较两个字符串剩下的字符是否一致就行了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=1000010;
int n,k;
char s[N],t[N],stk[N];
int cnt[N];
void op(char *ss)
{
	int top=0;
	memset(cnt,0,sizeof cnt);
	for(int i=1;i<=n;i++)
	{
		stk[++top]=ss[i];
		if(stk[top]==stk[top-1])cnt[top]=cnt[top-1]+1;
		
		else cnt[top]=1;
		
		if(cnt[top]==k)top-=k;
		
	}
	for(int i=1;i<=top;i++)ss[i]=stk[i];
	ss[top+1]='\0';
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>k;
	cin>>(s+1)>>(t+1);
	op(s);
	op(t);
	
	if(!strcmp(s+1,t+1))cout<<"Yes"<<"\n";
	else cout<<"No"<<"\n";
	return 0;
}

总结:字符串的操作寻找其本质,代码能力也很重要,就比如这题,栈的操作就非常巧妙。

  1. Pyramid 规律、打表

大意:给定一个 n 级三角形定义,找共含有多少个正三角形。

思路:没啥思路,打表找规律,把三角形放到坐标系里,暴力枚举三个顶点,当然也可以手算。

打出的表 :1 5 15 35 70 然而并没什么卵用,看不出什么规律。接下来介绍一个找规律的技巧,求差分:

1 5 15 35 70

1 4 10 20 35

1 3 6 10 15

1 2 3 4 5

1 1 1 1 1

直到最后所有公差为0。求了几次差分最高次幂就是几次。

然后从第一次差分开始,第 k 次差分取第k个数。 即 1 3 3 1 。前 n 项和公式即为 s ( n ) = 1 C n + 1 2 + 3 C n + 1 3 + 3 C n + 1 4 + 1 C n + 1 5 s(n)=1C_{n+1}^2+3C_{n+1}^3+3C_{n+1}^4+1C_{n+1}^5 s(n)=1Cn+12+3Cn+13+3Cn+14+1Cn+15

f ( n ) = s ( n ) − s ( n − 1 ) f(n)=s(n)-s(n-1) f(n)=s(n)s(n1)

最终求的 f ( n ) = n ( n + 1 ) ( n + 2 ) ( n + 3 ) 24 f(n)=\frac{n(n+1)(n+2)(n+3)}{24} f(n)=24n(n+1)(n+2)(n+3)

总结:收获了新的找规律求公式的方法。

  1. Magic Potion 二分图的最大匹配、匈牙利算法

大意:给定英雄和怪兽的二分图,每个英雄最最多杀死一只怪兽,现有 k 瓶药水,每个英雄最多使用一次药水,使用药水可以多杀死一只怪兽,问最多能杀死多少只怪兽。

思路:通过题目,如果不考虑药水,显然就是个二分图最大匹配的问题,但是多了药水的影响。想想匈牙利算法,跑一次就是一一对应的最大匹配数,在此基础上再跑一次,实际上就是一男最多可以匹配两女的最大匹配数。有了药水,也就是英雄最多杀死两只怪兽。所以根本不需要什么网络流,跑两遍匈牙利就行了,注意药水数量 k 的限制。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=510;

int n,m,k,match[N];
vector<int> G[N];
bool st[N];
bool find(int u)
{	
	for(auto v:G[u])
	{
		if(st[v])continue;
		st[v]=1;
		if(!match[v]||find(match[v]))
		{
			match[v]=u;
			return 1;
		}
	}
	return 0;
}
void solve()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	{
		int t;
		cin>>t;
		while(t--)
		{
			int x;
			cin>>x;
			G[i].push_back(x);
		}
	}
	
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		memset(st,0,sizeof st);
		if(find(i))ans++;
	}
	for(int i=1;i<=n&&k;i++)
	{
		memset(st,0,sizeof st);
		if(find(i))ans++,k--;
	}
	
	cout<<ans<<"\n";
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	int _=1;
	while(_--)solve();
	return 0;
}

总结:对于算法不能只是停留在会板子的水平,得真正了解算法的本质。就比如这题,理解了匈牙利算法的实质压根就不用网络流。

  1. Prime Game 数论、思维

思路:显然我们可以枚举质因子计算个数。如果我们可以在每个位置 i 枚举 a i a_i ai 的质因子,O(1)的计算出该质因子对最终结果的贡献就可以遍历去计算了。对于每个位置 i 的质因子 v, 假设它是第一次出现,不难计算出他对最终结果的贡献,即 (i-1) * (n-i+1) +n-i + 1

如果前面位置 j 已经出现过一次,那么他对最终结果的贡献即为 (i-j-1) * (n-i+1) + n-i+1

所以我们在更新过程中维护下某个质因子最后出现的下标就行了。需要注意的是,我们在筛质因子的时候只需要筛到1e3就行了,只需要用1e3以内的质数就可以求出1e6的所有质数。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1000010;
int a[N],pre[N],n;
vector<int> p;
bool st[N];
vector<int> g[N];
void solve()
{
	for(int i=2;i<=1e3;i++)
	{
		if(!st[i])p.push_back(i);
		
		for(int j=0;p[j]<=1e3/i;j++)
		{
			st[i*p[j]]=1;
			if(i%p[j]==0)break;
		}
	}
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		int y=a[i];
		if(g[y].size()||y==1)continue;
		for(auto x:p)
		{
			if(x>y)break;
			if(y%x==0)
			{
				g[a[i]].push_back(x);
				while(y%x==0)y/=x;
			}
		}
		if(y>1)g[a[i]].push_back(y);
	}
	LL ans=0;
	for(int i=1;i<=n;i++)
	{
		for(auto x:g[a[i]])
		{
			ans+=(LL)(i-pre[x]-1)*(n-i+1)+(n-i+1);
			pre[x]=i;
		}
	}
	cout<<ans<<"\n";
	
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	int _=1;
	//cin>>_;
	while(_--)solve();
	return 0;
}

总结:首先得想到从质因子的角度思考,然后去想办法推公式。

  1. Kangaroo Puzzle

题目也不叙述了,题目保证有解,然后就随机输出就能 过了,这…我看不懂,但我大为震撼。

  1. Mediocre String Problem 哈希+二分+马拉车

大意:给定字符串 s、t。在 s 中选择一个子串,t中选一个前缀串,s中选择的子串长度大于t的前缀串,并且使得其拼接是个回文串,求方案数

思路:首先的知道怎样选择可以符合题目描述的串,因为最终是回文串,我们直接在s,t里面选择不太好选,我们可以考虑把 t 串翻转,然后就等价于对 t 的后缀进行匹配,即选择 s的子串和 t 的后缀相等,然后 s 继续扩展的部分是回文串,这样构成的最终的串就是符合题目要求的。

先来看看怎么找相等的部分,我们可以用哈希+二分来做,字符串匹配是具有单调性的,即当前长度不能的匹配,那么更长的也必定不能匹配,(这个单调性只适用于在两个字符串的同一侧添加字符)。所以我们可以在 s 串中枚举终点,然后二分查找最长的起点,这样就能做到两个字符串都是在左侧添加字符,是可以二分的。

解决完匹配问题剩下的就是匹配完之后的往后扩展回文串的问题,想到高效处理所有的回文串,我们不难想到马拉车算法,马拉车算法处理的是以 i 为中心的回文串个数。而对于我们这里的问题,假设我们枚举到终点 i,我们需要的是以 i+1 为起点的回文串的个数。这也不难,马拉车算法求出中点左半边都可以作为一个回文串起点,相当于一个区间+1的操作,我们可以用一个差分数组实现。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int,int> PII;
const int N=1000010;
const ULL base=131;
char s[N],t[N],ss[N<<1];
ULL hs[N],ht[N],qs[N];
int n,m,d[N<<1],cnt[N];
void init()
{
	qs[0]=1;
	for(int i=1;i<=1e6;i++)qs[i]=qs[i-1]*base;
}
void make_hash(char *str,ULL *h)
{
	int len=strlen(str+1);
	for(int i=1;i<=len;i++)h[i]=h[i-1]*base+(ULL)str[i];
}
ULL get_hash(int l,int r,ULL *h)
{
	return h[r]-h[l-1]*qs[r-l+1];
}
void manacher(char *s)
{
	int id=0,len=strlen(s+1);
	ss[++id]='#';
	for(int i=1;i<=len;i++)
	{
		ss[++id]=s[i];
		ss[++id]='#';
	}
	for(int i=1,l=1,r=0;i<=id;i++)
	{
		int k=(i>r) ? 1 : min(d[l+r-i],r-i+1);
		while(i-k>=1&&i+k<=id&&ss[i-k]==ss[i+k])k++;
		k--;
		d[i]=k+1;
		if(i+k>r)
		{
			l=i-k;
			r=i+k;
		}
	}
	for(int i=1;i<=id;i++)
	{
		int pos=i>>1;
		int x=d[i]>>1;
		cnt[pos-x+1]++;
		cnt[pos+1]--;
	}
	for(int i=0;i<=n;i++)cnt[i]+=cnt[i-1];
	cnt[n+1]=0;
}
void solve()
{
	init();
	cin>>(s+1)>>(t+1);
	n=strlen(s+1);
	m=strlen(t+1);
	reverse(t+1,t+1+m);
	manacher(s);
	make_hash(s,hs);
	make_hash(t,ht);
	LL ans=0;
	for(int i=1;i<=n;i++)
	{
		int l=max(1,i-m+1),r=i;
		
		while(l<r)
		{
			int mid=l+r >> 1;

			if(get_hash(mid,i,hs)==get_hash(m-i+mid,m,ht))r=mid;
			else l=mid+1;
		}
		if(l==i&&s[i]!=t[m])continue;
		
		ans+=(LL)(i-l+1)*cnt[i+1];
	}
	cout<<ans<<"\n";

}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	int _=1;
	//cin>>_;
	while(_--)solve();
	return 0;
}

总结:多个算法的综合应用,思维难度还是挺大的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值