骗分与NOIP技巧 代码仓库

Sweetlemon

2018-10-31 14:49:51

Personal

## 例题 代码 ### 魔法阵 $10$分 ```cpp #include <iostream> using namespace std; int main(void){ int n,m; cin >> n >> m; for (int i=0;i<m;i++) cout << "0 0 0 0\n"; return 0; } ``` ### 小凯的疑惑 $30$分 ```cpp #include <iostream> using namespace std; int ok[10005]; //ok是标记数组,ok[i]==1表示金额i可以支付;数组开得比范围略大 int inf=10000; //假设不能支付的金额不会超过10000元 int max_coins=100; //假设每一种硬币支付的金额不会超过100枚 int main(void){ int a,b; int ans=1; //1一定无法支付,否则没有答案 cin >> a >> b; //枚举a,b两种硬币支付的数目 for (int i=0;i<=max_coins;i++) //支付a硬币数量 for (int j=0;j<=max_coins;j++) //支付b硬币数量 ok[a*i+b*j]=1; //支付i枚a硬币和j枚b硬币,是可以做到的 for (int i=0;i<=a*max_coins;i++) if (ok[i]==0) //表明i不能被支付 ans=i; //由于i是从小到大枚举的,因此i就是当前不能支付的最大金额 cout << ans << endl; return 0; } ``` ### 列队 $30$分 ```cpp #include <cstdio> using namespace std; int cube[1005][1005]; //记录每个位置上人的编号 int main(void){ int n,m,q; //n行, m列, q次查询 scanf("%d%d%d",&n,&m,&q); for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) cube[i][j]=(i-1)*m+j; //计算初始时每个人的编号 for (int i=0;i<q;i++){ int x,y,now; //(x,y)表示当前离队的人所在的行、列,now表示当前离队的人的编号 scanf("%d%d",&x,&y); now=cube[x][y]; printf("%d\n",now); //向左看齐 for (int j=y;j<m;j++) cube[x][j]=cube[x][j+1]; //把y右边的人向左移动一个位置 //向前看齐 for (int j=x;j<n;j++) cube[j][m]=cube[j+1][m]; //把x下面的、位于第m列的人向前移动一个位置 cube[n][m]=now; //离队的人回到第n行第m列 } return 0; } ``` ### 写文件 示例(提交到洛谷上会WA) ```cpp #include <cstdio> using namespace std; int main(void){ freopen("apb.in","r",stdin); freopen("apb.out","w",stdout); int a,b; scanf("%d%d",&a,&b); printf("%d\n",a+b); return 0; } ``` ### 天天爱跑步 $20$分 ```cpp #include <iostream> using namespace std; int w[1005]; //每个节点观察员的出现时间 int cnt[1005]; //每个节点观察员能够观察到的玩家数量 int main(void){ int n,m; //观察员的数量和玩家的数量 cin >> n >> m; for (int i=1;i<=n-1;i++){ int u,v; cin >> u >> v; //这个输入没用(骗分用不到) } for (int i=1;i<=n;i++) cin >> w[i]; //读入观察员出现的时间 for (int i=1;i<=m;i++){ int s,t; cin >> s >> t; //读入玩家的起点和终点 if (w[s]==0) //检查起点的观察员出现的时间是否为0 cnt[s]++; //如果为0,这个观察员能够观察到的玩家数目+1 } for (int i=1;i<=n;i++) cout << cnt[i] << ' '; return 0; } ``` ## 练习 代码 ### 棋盘 $5$分 考虑可能的无解情况。 ```cpp #include <iostream> using namespace std; int main(void){ cout << "-1\n"; return 0; } ``` ### 魔法阵 $40$分 ```cpp #include <cstdio> using namespace std; int x[40005]; //魔法值 int a[40005],b[40005],c[40005],d[40005]; //分别作为A,B,C,D物品出现的次数 int main(void){ int n,m; scanf("%d%d",&n,&m); for (int i=0;i<m;i++) scanf("%d",x+i); //这个读入方法初赛考过,相当于scanf("%d",&a[i]); for (int i=0;i<m;i++){ for (int j=0;j<m;j++){ if (i==j) continue; //一个物品不能同时作为A物品和B物品 for (int k=0;k<m;k++){ if (k==i || k==j) continue; //同理 for (int l=0;l<m;l++){ if (l==i || l==j || l==k) continue; //同理 //判断能否组成魔法阵 if (x[i]<x[j] && x[j]<x[k] && x[k]<x[l] && (x[j]-x[i])==2*(x[l]-x[k]) && (x[j]-x[i])*3<x[k]-x[j]){ //可以组成魔法阵,更新出现次数 a[i]++; b[j]++; c[k]++; d[l]++; } } } } } //输出 for (int i=0;i<m;i++) printf("%d %d %d %d\n",a[i],b[i],c[i],d[i]); return 0; } ``` 事实上,上面的代码可以进行简单优化,优化后可以多得$15$分。 优化的方法很简单:把判断尽可能提前,避免枚举一些不可能组成魔法阵的组合方式。 这种技巧我们称作“剪枝”,在做更难的题目的时候非常有用。 ```cpp #include <cstdio> using namespace std; int x[40005]; int a[40005],b[40005],c[40005],d[40005]; int main(void){ int n,m; scanf("%d%d",&n,&m); for (int i=0;i<m;i++) scanf("%d",x+i); for (int i=0;i<m;i++){ for (int j=0;j<m;j++){ if (i==j || x[i]>=x[j]) //x[i]>=x[j]则不可能组成魔法阵 continue; for (int k=0;k<m;k++){ if (k==i || k==j || x[j]>=x[k] || (x[j]-x[i])*3>=(x[k]-x[j])) //x[j]-x[i]*3>=x[k]-x[j]则不可能组成魔法阵 continue; for (int l=0;l<m;l++){ if (l==i || l==j || l==k || x[k]>=x[l]) continue; if ((x[j]-x[i])==2*(x[l]-x[k])){ a[i]++; b[j]++; c[k]++; d[l]++; } } } } } for (int i=0;i<m;i++) printf("%d %d %d %d\n",a[i],b[i],c[i],d[i]); return 0; } ``` ### 转圈游戏 $30$分 这题实际上是数学题,但这里我们采用暴力模拟的方法。 ```cpp #include <iostream> using namespace std; int main(void){ int n,m,x,k; //n个小伙伴,每次走m步,需要求的是x号小伙伴的位置,进行10^k轮 long long ans; //ans保存x号小伙伴的位置 long long turns=1; //turns计算游戏进行的轮数 cin >> n >> m >> k >> x; for (int i=1;i<=k;i++) turns=turns*10; //令turns=10^k ans=x; //游戏开始前在x号位置 for (int i=1;i<=turns;i++){ ans+=m; //移动m个位置 if (ans>=n) //ans>=n表示已经绕了一圈(注意位置从0开始编号) ans-=n; } cout << ans << endl; return 0; } ``` ### 线索 $40$分 这题实际上是数据结构题,这里我们也采用暴力模拟。 ```cpp #include <iostream> #include <algorithm> #define MAXN 5005 using namespace std; int color[5005]; //记录每条丝线的颜色 int temp_arr[5005]; //临时数组 int connect[5005][5005]={0}; //记录与每个点建立联系的点 int connect_num[5005]={0}; //记录与每个点建立联系的点的数量 int main(void){ int n,m; //n条丝线,m次操作 cin >> n >> m; for (int i=1;i<=n;i++) cin >> color[i]; //输入初始颜色 for (int i=0;i<m;i++){ int a,l,r,x; cin >> a >> l >> r >> x; switch (a){ case 1: //把从l到r的丝线以及与这些丝线建立联系的所有丝线染成颜色x for (int j=l;j<=r;j++){ //枚举区间内所有丝线 color[j]=x; //先把丝线本身染成x色 for (int k=0;k<connect_num[j];k++) //枚举与它建立联系的所有丝线connect[j][k] color[connect[j][k]]=x; //染色 } break; case 2: int is_connected=0; //记录l和r是否已经建立联系 color[l]=color[r]=x; //先对l,r进行染色 for (int k=0;k<connect_num[l];k++){ if (connect[l][k]==r){ //已经建立联系 is_connected=1; break; } } if (is_connected){ //如果已经建立联系,就只需要再染色就好了 for (int k=0;k<connect_num[l];k++) //枚举与l建立联系的所有丝线(也可以枚举与r建立联系的所有丝线) color[connect[l][k]]=x; //染色 continue; } int new_connect_num=0; //记录形成的新的结所含的丝线数目 for (int k=0;k<connect_num[l];k++){ temp_arr[new_connect_num]=connect[l][k]; //把与l建立联系的丝线放进临时数组 new_connect_num++; //更新形成的新的结所含的丝线数目 } for (int k=0;k<connect_num[r];k++){ temp_arr[new_connect_num]=connect[r][k]; //把与r建立联系的丝线放进临时数组 new_connect_num++; //更新形成的新的结所含的丝线数目 } //再把l和r放进临时数组,并更新丝线数目 temp_arr[new_connect_num]=l; new_connect_num++; temp_arr[new_connect_num]=r; new_connect_num++; //对新的结中所有丝线进行染色,并更新与之建立联系的丝线数组(connect数组)和丝线数目(connect_num数组) for (int j=0;j<new_connect_num;j++){ int now_process=temp_arr[j]; //记录当前处理的丝线编号 connect_num[now_process]=new_connect_num-1; //注意,减去1是因为丝线不能和自己建立联系 color[now_process]=x; //染色 int k=0; //k记录数组connect的下标 for (int t=0;t<new_connect_num;t++){ if (t==j) //不能把自己放进联系数组中 continue; connect[now_process][k]=temp_arr[t]; //把temp_arr[t]放入联系数组 k++; //移动到下一个位置 } } break; } } //输出每条丝线的颜色和与之建立联系的丝线数量 for (int i=1;i<=n;i++) cout << color[i] << ' '; cout << endl; for (int i=1;i<=n;i++){ cout << connect_num[i] << ' '; } cout << endl; return 0; } ``` ### 日常 $5$分 这题实际上是数学题。 直接输出所有盘子按顺序排列的方案。 ```cpp #include <iostream> #define MOD 993244853 using namespace std; int main(void){ int n; int c0; cin >> n >> c0; cout << c0%MOD << endl; for (int i=1;i<=n;i++) cout << i << ' '; return 0; } ``` ### 日常 $20$分 通过$\text{dfs}$,枚举所有摆盘子的方法,统计每个错乱数对应的摆法数;找到最优答案后,再$\text{dfs}$一次找到字典序最小的方案。 ### 探访 $5$分 这题实际上是图论题。 ~~有一点点~~太难了呢。 在图上进行$\text{dfs}$寻找最优方案。 ```cpp #include <iostream> #include <algorithm> #include <vector> //这个是vector(可以理解为变长数组) #define MAXN 10005 #define INF 100000000000000ll using namespace std; struct Edge{ int u,v,w,k; Edge(void):u(0),v(0),w(0),k(0){} Edge(int tu,int tv,int tw,int tk):u(tu),v(tv),w(tw),k(tk){} Edge(const Edge &te):u(te.u),v(te.v),w(te.w),k(te.k){} }; //边结构体 vector<Edge> g[MAXN]; int vis[MAXN]={0}; //记录某个点是否走过 typedef vector<Edge>::iterator iter; //迭代器(严重超纲) int n; int maxk,dis; //记录当前走过的路径的k的最大值和路径总长度 long long ans=INF; void dfs(int x); int main(void){ int m; cin >> n >> m; for (int i=0;i<m;i++){ int u,v,w,k; cin >> u >> v >> w >> k; g[u].push_back(Edge(u,v,w,k)); //这个是存图的方法(vector邻接表) } maxk=dis=0; dfs(1); //从1号节点开始枚举路径找答案 cout << ans << endl; return 0; } void dfs(int x){ if (x==n){ // 如果已经到达了n号节点 if ((long long)(maxk)*dis<ans) //更新答案 ans=(long long)(maxk)*dis; return; } for (iter it=g[x].begin();it!=g[x].end();it++){ if (!vis[it->v]){ //枚举从x节点出发的每一条边 //如果这条边的到达点没有被访问过,就访问它 vis[it->v]=1; dis+=it->w; //更新距离 int tmx=maxk; //保存原来的maxk,便于恢复 if ((it->k)>maxk) maxk=it->k; //更新maxk dfs(it->v); //dfs访问 maxk=tmx; //恢复原来的maxk dis-=it->w; //恢复dis变量的值 vis[it->v]=0; //恢复vis数组 } } } ``` ### 绝美的挣扎 $10$分 这题实际上是动态规划($\text{dp}$)题。 考虑$k_i=0$的情况。此时所有设施彼此不会互相影响,可以控制所有设施,所以答案为所有设施的重要度之和。 ```cpp #include <iostream> using namespace std; int w[1005]; int main(void){ int n; int ans=0; cin >> n; for (int i=0;i<n;i++){ cin >> w[i]; ans+=w[i]; } cout << ans << endl; return 0; } ``` ### 绝美的挣扎 $20$分 考虑$n\le 20$的情况,直接进行暴搜。再加上上面的$10$分,一共可以得到$20$分。 这种在一个程序里根据数据的特点、写两种不同做法的方法,叫做按数据分治。按数据分治是很重要的得分方法! 另外,下面的程序也可以进行剪枝优化,你知道怎么做吗? ```cpp #include <cstdio> #include <algorithm> using namespace std; int n; int maxans=0; int ans=0; int w[1005],k[1005]; int chose[1005]={0}; //记录方案,表示是否控制这个设施 void dfs(int x); int chk(void); //检查这种方案是否可行 int main(void){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",w+i); for (int i=1;i<=n;i++) scanf("%d",k+i); int special=1; //special=1表示所有k等于0的特殊情况 for (int i=1;i<=n;i++){ if (k[i]>0){ special=0; break; } } if (special){ //特殊情况直接求和 for (int i=1;i<=n;i++) maxans+=w[i]; printf("%d\n",maxans); return 0; } //否则爆搜 dfs(1); printf("%d",maxans); return 0; } int chk(void){ for (int i=1;i<=n;i++){ if (!chose[i]) continue; //枚举所有i可能影响到的设施 //注意是环形,要处理下标 for (int j=i-k[i];j<=i+k[i];j++){ int tj=j; //不要对循环变量进行操作,否则可能导致死循环 if (tj<=0) //小于0表示转回头了,要+n tj+=n; else if (tj>n) //大于n表示转了一圈,要-n tj-=n; if (tj!=i && chose[tj]) //如果选择了干扰范围内的另一个设施,说明该方案不可行 return 0; } } //方案可行 return 1; } void dfs(int x){ if (x>n){ //已经枚举完毕 if (chk()) //如果方案合法 maxans=max(maxans,ans); //更新答案 return; } //不选x号设施 dfs(x+1); //选择x号设施 chose[x]=1,ans+=w[x]; dfs(x+1); chose[x]=0,ans-=w[x]; //记得还原哦 } ```