由于信息学竞赛的特殊性:黑箱评测,只用有限的一些数据来判断选手的程序是否正确。再加上 CCF(Coin Collect Foundation) 用脚造数据,所以在 NOIp/CSP 中,如果事先了解一些骗分技巧,往往可以得到非常高的分数。
在 OI 界已经有过名为《骗分导论》的论文,但作者个人认为该论文的骗分技巧不多,而且论文产生于若干年前,有点过时。因此本人想再写一篇类似《骗分导论》的文章,分享一些骗分技巧。
由于作者个人水平有限,所以如果各位有其他的骗分技巧,也欢迎分享出来。你可以在评论区里回复你的技巧,我会把你分享的内容加入本文章中。本文章将持续更新。
在随机化算法中,如果我们不断地随机,那么得到的结果会越来越接近最后的正确答案。但过多地随机,就会使程序tle。我们可以在程序中对当前已经运行的时间进行判断,如果已经超时就不再进行随机。
卡时往往与随机化算法结合会得到非常好的效果,我们在后面讲解一些随机化的做法时再细讲。
下面给出一个卡时的例子。
例1: CSP-S 2022 T3 星战
如果我们发现如果所有据点可以“连续穿梭”,则一定可以“实现反击”,那么本题的暴力就可以非常简单实现:
cpp12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#include<bits/stdc++.h>
#define _ 500005
#define int long long
#define ull unsigned long long
using namespace std;
const int mod=0;
inline int read(){
int res=0,ch=getchar(),f=1;
while(!isdigit(ch) and ch!=EOF){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
res=(res<<3)+(res<<1)+(ch-'0');
ch=getchar();
}
return res*f;
}
struct Edge{int next,to;} edge[_];
int out[_],head[_],size,ans;
inline void adde(int u,int v){edge[++size].to=v,edge[size].next=head[u],head[u]=size;}
set<int> tag[_];
inline void del(int u,int v){
if(tag[v].count(u)) return;
tag[v].insert(u);
int flag1=out[u];
out[u]--;
int flag2=out[u];
if(flag1==1&&flag2!=1) ans--;
if(flag1!=1&&flag2==1) ans++;
}
inline void add(int u,int v){
if(tag[v].count(u)==0) return;
tag[v].erase(u);
int flag1=out[u];
out[u]++;
int flag2=out[u];
if(flag1==1&&flag2!=1) ans--;
if(flag1!=1&&flag2==1) ans++;
}
signed main(){
freopen("galaxy.in","r",stdin);
freopen("galaxy.out","w",stdout);
int n=read(),m=read();
for(int i=1;i<=m;i++){
int u=read(),v=read();
out[u]++;
adde(v,u);
}
for(int i=1;i<=n;i++) if(out[i]==1) ans++;
int q=read();
while(q--){
int t=read(),u,v;
if(t==1||t==3) u=read(),v=read();
if(t==2||t==4) u=read();
if(t==1){
del(u,v);
}else if(t==2){
for(int i=head[u];i;i=edge[i].next){
del(edge[i].to,u);
}
}else if(t==3){
add(u,v);
}else{
for(int i=head[u];i;i=edge[i].next){
add(edge[i].to,u);
}
}
if(ans==n) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
可以得到 60 pts
我们继续观察大样例,发现最终结果大部分都是 NO。对于最后几个数据规模较大,暴力肯定超时的点,我们与其tle,不如直接全输出NO。
为了更好地发挥暴力算法的作用,我们可以这样做:执行暴力算法,程序卡时,快要超时就在后面全输出NO。
cpp1234567 while(q--){
if((double)clock()/CLOCKS_PER_SEC>1.8){
printf("NO\n");
continue;
}
...
}
在CCF的数据下可以得到80分。
在这里值得一提的是,由于操作次数过多,所以会导致卡时不准。比如上述代码,卡时1.8s,实际测试点最大的运行时间达到了1.9s。如果直接卡1.9s,很可能与暴力同分。我们可以把最后全输出NO的情况单独拿出来写,具体如下
cpp1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586#include<bits/stdc++.h>
#define _ 500005
#define int long long
#define ull unsigned long long
using namespace std;
const int mod=0;
inline int read(){
int res=0,ch=getchar(),f=1;
while(!isdigit(ch) and ch!=EOF){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
res=(res<<3)+(res<<1)+(ch-'0');
ch=getchar();
}
return res*f;
}
struct Edge{int next,to;} edge[_];
int out[_],head[_],size,ans;
inline void adde(int u,int v){edge[++size].to=v,edge[size].next=head[u],head[u]=size;}
set<int> tag[_];
inline void del(int u,int v){
if(tag[v].count(u)) return;
tag[v].insert(u);
int flag1=out[u];
out[u]--;
int flag2=out[u];
if(flag1==1&&flag2!=1) ans--;
if(flag1!=1&&flag2==1) ans++;
}
inline void add(int u,int v){
if(tag[v].count(u)==0) return;
tag[v].erase(u);
int flag1=out[u];
out[u]++;
int flag2=out[u];
if(flag1==1&&flag2!=1) ans--;
if(flag1!=1&&flag2==1) ans++;
}
signed main(){
freopen("galaxy.in","r",stdin);
freopen("galaxy.out","w",stdout);
int n=read(),m=read();
for(int i=1;i<=m;i++){
int u=read(),v=read();
out[u]++;
adde(v,u);
}
for(int i=1;i<=n;i++) if(out[i]==1) ans++;
int q=read();
while(q--){
if((double)clock()/CLOCKS_PER_SEC>1.9){
printf("NO\n");
break;
}
int t=read(),u,v;
if(t==1||t==3) u=read(),v=read();
if(t==2||t==4) u=read();
if(t==1){
del(u,v);
}else if(t==2){
for(int i=head[u];i;i=edge[i].next){
del(edge[i].to,u);
}
}else if(t==3){
add(u,v);
}else{
for(int i=head[u];i;i=edge[i].next){
add(edge[i].to,u);
}
}
if(ans==n) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
//注意这里
while(q>0){
printf("NO\n");
q--;
}
return 0;
}
我们这里并不用贪心算法去想正解,而是灵活运用贪心的思想来骗分
例1: [USACO18OPEN] Talent Show G
首先我们考虑一个贪心,可能不一定正确。我们把每个牛的才艺与重量的比值从大到小进行排序。一个直观的想法是选择性价比高的牛可能会更好一些。然后我们dfs爆搜。
这里有个做法,就是当遍历到第i个牛时,不去立即选择第i+1个牛,而是向后枚举接下来要选择的牛。
cpp12345678910111213141516171819202122232425#include<bits/stdc++.h>
#define _ 500
using namespace std;
int n,w;
typedef pair<int,int> pii;
pii e[_];
int ans,cnt=0;
void dfs(int x,int nowt,int noww){
cnt++;
if(cnt>100000000) return;
if(noww>=w) ans=max(ans,nowt*1000/noww);
for(int i=x+1;i<=n;i++) dfs(i,nowt+e[i].first,noww+e[i].second);
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>w;
for(int i=1;i<=n;i++) cin>>e[i].second>>e[i].first;
sort(e+1,e+n+1,[](pii x,pii y){return x.first*y.second>y.first*x.second;});
for(int i=1;i<=n;i++) dfs(i,e[i].first,e[i].second);
cout<<ans;
return 0;
}
结合卡时,上述的代码就可以得到50 pts。
然后我们接下来考虑,因为我们是按照性价比从高到低的顺序选择,如果选择的牛的重量已经满足要求,接下来再去选择,大概率是不会让答案更优的,所以我们将上述代码中的
cpp1if(noww>=w) ans=max(ans,nowt*1000/noww);
改为
cpp1if(noww>=w) return ans=max(ans,nowt*1000/noww),void();
就可以直接ac 当然这一步风险还是有的