1.计数

(count.cpp/c/pas)
时间限制:1s
内存限制:256MB
【问题描述】
给出 m 个数 a[1],a[2],…,a[m]
求 1~n 中有多少数不是 a[1],a[2],…,a[m]的倍数。
【输入】
输入文件名为 count.in。
第一行,包含两个整数:n,m
第二行,包含 m 个数,表示 a[1],a[2],…,a[m]
【输出】
输出文件名为 count.out。
输出一行,包含 1 个整数,表示答案
【输入输出样例】

count.in count.out
10 2
2 3
3

【数据说明】
对于 60%的数据,1<=n<=1e6
对于另外 20%的数据,m=2
对于 100%的数据,1<=n<=1e9,0<m<=20,1<=a[i]<=1e9

期望得分:80分

实际得分:20分

这是一个被STL生生卡死的故事。。。。

第一题怎么是个数论啊...慌了慌了。虽然现在在复习数论,但是也不太会呀qwq。

60分应该是比较好得的,我们只要暴力判断一下就行了。但是不知道为什么,可能是受了昨天看的素数筛法的影响,我想到的是标记a数组中数的倍数。算法正确性显然。但是我当时脑子抽了,竟然想用这个跑100%的数据,真是1秒过1e9啊,其实1秒跑百万都费劲,于是感觉普通的数组1e9开不下,开始用vector+map。。。。结果常数巨大,60%的点完全跑不过....真可谓:穿着衣服洗澡。本应用60分的数据求稳把数组开到1000000就行了...稳过...

还好我还有梦想,写完第二题第三题的暴力后回过头来继续看T1,打算优化一下算法,实在优化不动就开始看20分的特殊性质m==2.想了一会发现应该可以从gcd,lcm入手,分类讨论,答案加加减减倍数,不过这个时候又要收卷了...所以只打了给出的两个数互质的情况,草草交了上去。后来发现自己的思想已经很接近正解了,我那个加加减减的过程正是容斥原理

评测发现60分卡没了,然后还好最后写的特殊性质过了两个点,(还好数据水,两个数恰好是互质的情况)。

 #include<cstdio>
#include<algorithm>
#include<vector>
#include<map> using namespace std;
typedef long long ll; ll n,m,ans,tot;
ll g[];
vector<ll>flag;
map<ll,ll>ma; ll gcd(ll a,ll b)
{
return b ? gcd(b,a%b) : a;
} int main()
{
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%lld%lld",&n,&m);
for(ll i=;i<=m;i++)
{
ll x=;
scanf("%lld",&x);
if(x==)
{
printf("0\n");
return ;
}
g[i]=x;
}
/* if(m==1)
{
if(g[1]>n) printf("%lld",n);
else printf("%lld",n-n/g[1]);
return 0;
}*/
if(m==)
{
if(gcd(g[],g[])==)
{
if(g[]*g[]>n)
{
ans+=n/g[];
ans+=n/g[];
printf("%lld",n-ans);
}
else if(g[]*g[]<n)
{
ll lcm=g[]*g[];
ans+=n/g[];
ans+=n/g[];
ans-=n/lcm;
printf("%lld",n-ans);
}
else if(g[]*g[]==n)
{
ans+=n/g[];
ans+=n/g[];
ans--;
printf("%lld",n-ans);
}
return ;
}
/*else
{
ll lcm=g[1]*g[2]/gcd(g[1],g[2]);
//printf("lcm:%d\n",lcm);
if(lcm<n)
{
ans+=lcm/g[1];
ans--;
ans+=lcm/g[2];
ans+=n/lcm;
ans--;
printf("%lld",n-ans);
}
else if(lcm>n)
{
ans+=lcm/g[1];
ans+=lcm/g[2];
printf("%lld",n-ans);
}
else if(lcm==n)
{
ans+=lcm/g[1];
ans--;
ans+=lcm/g[2];
printf("%lld",n-ans);
}
}*/
}
for(int i=;i<=m;i++)
for(int j=;j<=n/g[i];j++)
if(!ma[g[i]*j]) flag.push_back(g[i]*j),ma[g[i]*j]=++tot;
printf("%lld\n",n-tot);
return ;
}

考场--20pts count

 #include<cstdio>
#include<algorithm> using namespace std;
typedef long long ll; ll n,m,ans,tot;
ll g[];
bool ma[]; ll gcd(ll a,ll b)
{
return b ? gcd(b,a%b) : a;
} int main()
{
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%lld%lld",&n,&m);
for(ll i=;i<=m;i++)
{
ll x=;
scanf("%lld",&x);
if(x==)
{
printf("0\n");
return ;
}
g[i]=x;
}
if(m==)
{
if(g[]>n) printf("%lld",n);
else printf("%lld",n-n/g[]);
return ;
}
if(m==)
{
if(gcd(g[],g[])==)
{
if(g[]*g[]>n)
{
ans+=n/g[];
ans+=n/g[];
printf("%lld",n-ans);
}
else if(g[]*g[]<n)
{
ll lcm=g[]*g[];
ans+=n/g[];
ans+=n/g[];
ans-=n/lcm;
printf("%lld",n-ans);
}
else if(g[]*g[]==n)
{
ans+=n/g[];
ans+=n/g[];
ans--;
printf("%lld",n-ans);
}
return ;
}
/*else
{
ll lcm=g[1]*g[2]/gcd(g[1],g[2]);
//printf("lcm:%d\n",lcm);
if(lcm<n)
{
ans+=lcm/g[1];
ans--;
ans+=lcm/g[2];
ans+=n/lcm;
ans--;
printf("%lld",n-ans);
}
else if(lcm>n)
{
ans+=lcm/g[1];
ans+=lcm/g[2];
printf("%lld",n-ans);
}
else if(lcm==n)
{
ans+=lcm/g[1];
ans--;
ans+=lcm/g[2];
printf("%lld",n-ans);
}
}*/
}
for(int i=;i<=m;i++)
for(int j=g[i];j<=n;j+=g[i])
if(!ma[j]) ma[j]=,tot++;
printf("%lld\n",n-tot);
return ;
}

优化后的暴力--80pts

正解:容斥原理

我们考虑最后的答案是$n-(n/a[1]+n/a[2]+n/a[3]+n/a[...m])$,但是我们很容易发现这样会加多,那么我们可以对于所有的二元组,减上$n/lcm(a[i],a[j]$,但是发现这样又会减多,于是再加去所有三元组的这个值。直到n。发现遇到奇数个数就加,遇到偶数个数就减。

为了不重不漏地枚举n元组,又注意到m<=20,是个二进制枚举的好东西我怎么没想到,所以我们就可以开始进行容斥原理的操作了。

这里有两种方式,二进制枚举和搜索,由于我太菜,选择了前者。

在开始之前,我们还需要思考一个问题:如何计算多个数的lcm?你不会说把他们乘起来再除以他们的公共gcd吧... 其实我们手动枚举一下,k个数的lcm球阀:k-1个数的乘积乘第k个数除以gcd(第k个数,k-1个数的乘积)。(Chemist就以为是第一种hhh)

 #include<cstdio>
#include<algorithm> using namespace std;
typedef long long ll; int n,m,fake,ans;
int g[]; ll gcd(ll a,ll b)
{
return b ? gcd(b , a % b) : a;
} int main()
{
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=;i<=m;i++)
{
scanf("%d",&g[i]);
if(g[i]==){printf("");return ;}
}
sort(g+,g+m+);//确保之后的大小单调性 利于剪枝
fake=(<<m)-;
for(int i=;i<=fake;i++)
{
int num=;//统计这个状态出现了多少1
ll lcm=;//在求lcm
bool flag=;
for(int j=;j<=m;j++)
if(i&(<<(j-)))//如果这一位是1
{// cut branches
if(1ll*(lcm+g[j])>1ll*n*) {flag=;break;}//剪枝 防止溢出
lcm=lcm*g[j]/gcd(lcm,g[j]);
if(lcm>n) {flag=;break;}//剪枝 防止溢出
num++;
}
if(!flag)
{
if(num&) ans+=n/lcm;//奇加偶减
else ans-=n/lcm;
}
}
printf("%d\n",n-ans);
return ;
}

AC--count

* Hint:其实离正解很近了233,多思考一下。打暴力一定要求稳啊,慎用STL。


2.区间第 k 大

(kth.cpp/c/pas)时间限制:1s

内存限制:256MB
【问题描述】
一个区间的价值定义为该区间中的最大值减最小值
给定 n 个数,求所有区间价值中,第 k 大值为多少。
【输入】
输入文件名为 kth.in。
第 1 行两个数 n、k($k<=n*(n-1)/2$)
第 2 行 n 个数,每个数<=109
【输出】
输出文件名为 kth.out。
输出区间价值的第 k 大值。
【输入输出样例】

kth.in kth.out
3 2
2 1 3
2

【样例解释】
[l,r]表示第 l 个数到第 r 个数组成的区间的价值
[1,1]=0 [1,2]=1 [1,3]=2
[2,2]=0 [2,3]=2
[3,3]=0
【数据说明】
对于 30%的数据,n=500
对于 60%的数据,n<=5000
对于 100%的数据,n<=400000

期望得分:60分

实际得分:30分

这个锅我不背,机房其他dalao以及本蒟蒻吸取昨天教训不用线段树改用st表后,依然不能得到60分,应该是多组数据的锅吧...

 #include<cstdio>
#include<algorithm>
#include<cmath> using namespace std; int n,k,tot;
int seq[],fa[][],fb[][]; int sta(int l,int r)
{
int hu=log2(r-l+);
return max(fa[l][hu],fa[r-(<<hu)+][hu]);
} int stb(int l,int r)
{
int hu=log2(r-l+);
return min(fb[l][hu],fb[r-(<<hu)+][hu]);
} bool cmp(int x,int y)
{
return x>y;
} int main()
{
freopen("kth.in","r",stdin);
freopen("kth.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=;i<=n;i++) scanf("%d",&fa[i][]),fb[i][]=fa[i][];
for(int j=;j<=;j++)
for(int i=;i+(<<j)-<=n;i++)
{
fa[i][j]=max(fa[i][j-],fa[i+(<<(j-))][j-]);
fb[i][j]=min(fb[i][j-],fb[i+(<<(j-))][j-]);
}
for(int i=;i<=n;i++)
for(int j=i;j<=n;j++)
seq[++tot]=sta(i,j)-stb(i,j);
//printf("%d %d\n",sta(i,j),stb(i,j));
sort(seq+,seq++tot,cmp);
printf("%d",seq[k]);
return ;
}

30pts--kth

正解:单调队列

首先我们考虑这样一个性质:对于一个固定的右端点r,我们设[l,r]的价值为w1,设[l-1,r]的价值为w2,他们的大小是否有可能进行比较?有的。显然有w2>=w1.为什么呢?我们考虑左端点向左移后,如果出现的这个值大于原区间的最大值,将他替换为区间最大值;小于原区间的最小值,将他替换为区间最小值;这个值在原区间最大值与最小值区间则不做影响。也就是说,左端点越靠左,越有可能(有潜力)出现最值,越有潜力使价值变得更大。

这个故事隐约告诉我们这是有二分单调性的。没错,这题我们的思路就是二分答案。二分什么?就是我们要求的第k大区间最大价值。如何写check函数?中心思路就是判断这个二分出的答案能否达到第k大。(若能,继续向大里找,不断二分缩小范围提高精度,反之同理。)

然后我们需要明确一点,因为我们当前二分出的值是第k大的,所以我们的核心做法就是计数所有比当前二分出的答案大的区间个数。这样一来,我们就也需要及时舍弃那些比当前二分出的答案小的价值。

因为这道题的特殊性,区间的价值定义为最大价值-最小价值。所以我们每刻都需要对最值掌握于心。单调队列是个很棒的选择。维护两个单调队列(一个递减,一个递增)他们的队首就是我们维护的最值(下标)。具体实现中可用STL中的deque来实现。

这是这道题的大概思路。在此特别感谢金涛dalao不厌其烦的讲解以及详细的代码注释Orz..

另外的一些细节在注释中...

细节:开始访问无效内存了==因为把

while(!qmax.empty()&&a[j]>=a[qmax.back()]) 

的判断条件先写的后者,因为&&是先判断左边的,所以后者可能会内存泄漏。

这是一个很大的问题

 #include<cstdio>
#include<algorithm>
#include<queue>
#define maxn 400090 using namespace std; int n,k,l,r;
int a[maxn]; bool check(int x)
{
int sum=,head=;
//维护两个单调队列,一大一小
deque<int>qmax;//单调递减队列 队首是最大值
deque<int>qmin;//单调递增队列 队首是最小值
for(int j=;j<=n;j++)
{
while(!qmax.empty()&&a[j]>=a[qmax.back()])
qmax.pop_back();
qmax.push_back(j);
while(!qmin.empty()&&a[j]<=a[qmin.back()])
qmin.pop_back();
qmin.push_back(j);//维护单调队列
while(!qmin.empty()&&!qmax.empty())
{
if(a[qmax.front()]-a[qmin.front()]<x) break;
//发现小于二分出的值直接退出
//因为当前二分出的值是第k大,所以在它之前的一定比他大
head++;//开始缩小左端点 (左端点靠左的一定区间价值会更大)
if(qmin.front()<head) qmin.pop_front();
if(qmax.front()<head) qmax.pop_front();
//对于位置<l的元素全部出队(这部分类似于滑动窗口)
}
sum+=head-;
//最终能有多少合法的位置,累加
}
return sum>=k;
} int main()
{
freopen("kth.in","r",stdin);
freopen("kth.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=;i<=n;i++) scanf("%d",&a[i]),r=max(a[i],r);
while(l<r)
{
int mid=(l+r+)>>;
if(check(mid)) l=mid;
else r=mid-;
}
printf("%d",l);
return ;
}

kth

然而这个代码只能过60分的...剩下4个点会超时的。主要原因是用的stl-deque...听dalao说这个比普通的queue会慢很多,所以下次再遇到这种情况还是用数组模拟比较好...而且这个题要开long long..果然AC不易啊....


3.武器分配

(submax.cpp/c/pas)

时间限制:1s
内存限制:256MB
【问题描述】
有 n 个堡垒排成一排构成了一条防御线。现在需要将 n 个武器放入这 n 个堡垒中,每个
堡垒放一个,每个武器有攻击力和战场贡献值两个属性。
由于这 n 个武器都不是人为操控的,所以会对其某半径内所有单位进行攻击,而这就导
致某些堡垒的互相攻击。现在发现第 i 个堡垒会和第 j 个堡垒互相攻击当且仅当|i-j|<=r,
且攻击力较低的武器和他所在的堡垒会破损。
现在你需要给出一种武器分配方案使得未破损武器的战场贡献值总和最大。为了方便你
只需输出战场贡献值总和的最大值即可。
【输入】
输入文件名为 submax.in。
第一行一个数 T(<=10),表示数据组数
对于每一组数据:
第一行两个数 n,r
第二行 n 个数,表示 1~n 号武器的攻击力
第三行 n 个数,表示 1~n 号武器的战场贡献值
【输出】
输出文件名为 submax.out。
对于每组数据输出一个数,表示答案
【输入输出样例】

submax.in submax.out
1
4 2
1 2 3 4
1 2 3 4
7

【数据范围】
对于 30%的数据:n<=10
对于 50%的数据,n<=16
对于 100%的数据 , n<=5000,武器攻击力 <=100000 且两两不同 ,武器的战场贡献值
<=100000,r<n

期望得分:30分

实际得分:0分

30(20)分算法

观察到50%的数据n<=16,可以考虑二进制枚举。

看了一会题后发现好像应该用的是全排列枚举,二进制行不通了。那就是30分了嘤嘤嘤,开始写,期间有一个边界细节没注意,改后,过了样例,试了几组自造小数据后,开始去 挽救第一题。

考场上我的思路开始还要对每个武器他们的价值从大到小排序,后来把自己绕晕了,便放弃了这个可能的剪枝。对于每一次生成的排列,我们都枚举它的左右。但是在这里!题目审错了!考试的时候我(基于现实生活),认为每个武器被消灭后就不能再打别人了。但实际上是可以的。因为我们也不知道他们开始打的顺序啊qwq。注释掉三条语句后有了20分。(因为题目是多组数据,而且卡10的阶乘,所以理论上30分是可以,但实际上行不通【被卡常了....】)

 #include<cstdio>
#include<algorithm>
#include<cstring> using namespace std; int T,n,r,ans;
int a[],b[];
int val[],att[],vis[]; void work()
{
int tmp=;
for(int i=;i<=n;i++)
{
int id=a[i],pos=i;
//if(vis[id]) continue;
while(i-pos<r&&pos->)
{
pos--;
int iid=a[pos];
//if(vis[iid]) continue;
if(att[id]>att[iid]) vis[iid]=;
else
{
vis[id]=;
break;
}
}
pos=i;
while(pos-i<r&&pos+<n)
{
pos++;
int iid=a[pos];
//if(vis[iid]) continue;
if(att[id]>att[iid]) vis[iid]=;
else
{
vis[id]=;
break;
}
}
}
for(int i=;i<=n;i++)
if(!vis[i]) tmp+=val[i];
ans=max(ans,tmp);
memset(vis,,sizeof(vis));
} void dfs(int x)
{
if(x==n+)
{
work();
return ;
}
for(int i=;i<=n;i++)
if(!b[i])
{
a[x]=i;
b[i]=;
dfs(x+);
a[x]=;
b[i]=;
}
} int main()
{
freopen("submax.in","r",stdin);
freopen("submax.out","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&r);
for(int i=;i<=n;i++) scanf("%d",&att[i]);
for(int i=;i<=n;i++) scanf("%d",&val[i]);
dfs();
printf("%d\n",ans);
ans=;
memset(a,,sizeof(a));
memset(b,,sizeof(b));
}
return ;
}

20pts--submax

50分算法:还没写完,鸽了。今天写上。

小结

期望得分:80+60+30=170

实际得分:20+30+0=50 落差还是比较大的qwq。

这次的问题还是出现在暴力分没有打满,该拿到的分拿不满,与预期相差太多了..

T1:拿好暴力分,T3:审题。关于卡常数的问题....可能我人弱常数大吧..不过还要慎用stl,能用数组代替的就用自带大常数的容器。这两次考试下来还有17、15的真题模拟赛中,暴力水平涨了不少。但是还是不够,学习更多的芝士能提高自己的暴力水平。

然后终于到了我脐带已久的自由复习时间,多次考试中暴露的问题还是基础知识不牢固,有些算法考场上会忘记实现。终于有机会补上,不咕了qwq。

05-11 21:47