- Adrien and Austin 博弈、思维
大意:给定 n 个石子,下标1-n。两人轮流取石子。最少1个最多k个,并且取的石子下标连续,不能操作的判输
思路:当 k!=1 时,显然先手可以去中间一个或两个使得石子分成对称的两段,然后一直做和后手一样的操作就行了。先手必胜。
当 k=1 时每次只能取一个,分就讨论就行了。注意这题比较坑的一个地方是n是可以等于0的。
- 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倍不止,随机大法好哇!!而且还不容易写错。当然如果实在脸黑还是得老老实实写三分。
- 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;
}
总结:字符串的操作寻找其本质,代码能力也很重要,就比如这题,栈的操作就非常巧妙。
- 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(n−1)
最终求的 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)
总结:收获了新的找规律求公式的方法。
- 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;
}
总结:对于算法不能只是停留在会板子的水平,得真正了解算法的本质。就比如这题,理解了匈牙利算法的实质压根就不用网络流。
- 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;
}
总结:首先得想到从质因子的角度思考,然后去想办法推公式。
题目也不叙述了,题目保证有解,然后就随机输出就能 过了,这…我看不懂,但我大为震撼。
- 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;
}
总结:多个算法的综合应用,思维难度还是挺大的。