T1:给一个方程 ax+by=c ,求它的正整数解的数量,其中 a,b,c 可以为负。
仿佛没有学扩展欧几里得一样,写了半天却有很多情况没有考虑到,才50分;发现了自己思维的不周密啊...
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; int T; ll a,b,c; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} ll abss(ll x){return x<0?-x:x;} void exgcd(ll a,ll b,ll &x,ll &y) { if(!b){x=1; y=0; return;} exgcd(b,a%b,x,y); ll t=x; x=y; y=t-a/b*y; } int main() { freopen("fuction.in","r",stdin); freopen("fuction.out","w",stdout); scanf("%d",&T); while(T--) { scanf("%lld%lld%lld",&a,&b,&c); if((a>0&&b>0&&c>0)||(a<0&&b<0&&c<0)) { if(a<0)a=-a,b=-b,c=-c; ll g=gcd(a,b),x,y; if(c%g){printf("0\n"); continue;} a/=g; b/=g; c/=g; exgcd(a,b,x,y); x*=c; y*=c; if(x>0) { ll k=x/b; x-=k*b; y+=k*a; if(y<0){printf("0\n"); continue;} } else { ll k=abss(x)/b+1; x+=k*b; y-=k*a; if(y<0){printf("0\n"); continue;} } ll ans; if(y%a==0)ans=y/a; else ans=y/a+1; if(ans>65535)printf("ZenMeZheMeDuo\n"); else printf("%d\n",ans); } else { if((a>0&&b>0&&c<0)||(a<0&&b<0&&c>0)){printf("0\n"); continue;} ll aa=abss(a),bb=abss(b),cc=abss(c); ll g=gcd(aa,bb); if(cc%g==0)printf("ZenMeZheMeDuo\n"); else printf("0\n"); } } return 0; }
然后观摩满分的程序,模仿它的众多细节,其中还有些自己目前尚不甚明晰的地方;
大概就是要注意先把 a,b,c 都变成正数,让 exgcd 处理正数,然后再把它取负回来;
负数的 gcd 那一块也不好弄,干脆不要写需要用 gcd 的扩展欧几里得,而直接在递归的底部赋值 x = c/a;
先把0的情况特判掉,这个还挺好判的;
还有个小发现,就是 a,b 异号的话有无穷多的解,因为原本的 x+b , y-a 因为异号都变成了正的,所以有无数个正整数解;
最后要注意 y 不合法的情况不仅是 y<=0,要注意 y 的最小正整数解是多少,所以还是从解的角度出发比较不容易错;
要怎样才能在考场上就把所有的细节正确地想出来呢?果然还是理解不透彻吧;
总之,感觉通过这一道考察基础的题,对扩展欧几里得的理解又加深了。
改后(AC)代码如下:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; ll T,a,b,c; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} void exgcd(ll a,ll b,ll &x,ll &y) { if(!b){x=c/a; y=0; return;}//不得到 gcd 的解,而直接令 x=c/a? exgcd(b,a%b,y,x); y-=a/b*x; } int main() { freopen("fuction.in","r",stdin); freopen("fuction.out","w",stdout); scanf("%lld",&T); while(T--) { scanf("%lld%lld%lld",&a,&b,&c); if(c<0)a=-a,b=-b,c=-c; if(a==0&&b==0) { if(c==0)printf("ZenMeZheMeDuo\n"); else printf("0\n"); continue; } if(a==0) { // if(c%b==0&&c/b>0)printf("1\n"); if(c%b==0&&c/b>0)printf("ZenMeZheMeDuo\n");//!!! //可取任意个0 else printf("0\n"); continue; } if(b==0) { // if(c%a==0&&c/a>0)printf("1\n"); if(c%a==0&&c/a>0)printf("ZenMeZheMeDuo\n");//!!! else printf("0\n"); continue; } bool f1=0,f2=0; if(a<0)f1=1,a=-a; if(b<0)f2=1,b=-b; ll g=gcd(a,b),x,y; // a/=g; b/=g; c/=g;//和平常 exgcd 写法不同? exgcd(a,b,x,y);// // x*=c; y*=c; a*=g; b*=g; c*=g; if(a*x+b*y!=c){printf("0\n"); continue;}//等价于 c%g!=0 ,无解? if(f1)a=-a,x=-x; if(f2)b=-b,y=-y; if(a*b<0) { // if(c%g)printf("0\n"); // else //可有可无 printf("ZenMeZheMeDuo\n");//发现能增加任意成比例倍 continue; } if(a<0)a=-a,b=-b,c=-c;//此时 a,b 同号 a/=g; b/=g; c/=g;// // if(x<0) //原来写法的残渣(为什么不行?(少了个特判?)) // { // ll k=(-x)/b+1; // x+=k*b; y-=k*a; // } // else // { // ll k=x/b; // x-=k*b; y+=k*a; // } // if(y<=0){printf("0\n"); continue;} // // ll ans; // if(y%a==0)ans=y/a; // else ans=y/a+1; ll ans; x=(x%b+b)%b; if(x==0)x=b; y=(c-a*x)/b; ll mny=(y%a+a)%a; if(mny==0)mny=a; if(mny>y)ans=0;//不仅是 y<=0 else ans=(y-mny)/a+1; if(ans>65535)printf("ZenMeZheMeDuo\n"); else printf("%lld\n",ans); } return 0; }
T2:树上染色,把 m 个点染成黑色,其它点是白色,收益是白点两两之间距离和+黑点两两之间距离和,求最大收益。
以前做过的题,有个很妙的性质是一旦确定子树内部有多少黑点,就知道子树内部有多少白点,以及子树外的白点、黑点个数;
所以就可以依据这个算出子树根上这条边的贡献,于是问题成了个简单的树形DP,考场AC。
考场(AC)代码如下:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; int const maxn=2005; int n,m,hd[maxn],ct,siz[maxn]; ll f[maxn][maxn]; struct N{ int to,nxt,w; N(int t=0,int n=0,int w=0):to(t),nxt(n),w(w) {} }ed[maxn<<1]; void add(int x,int y,int z){ed[++ct]=N(y,hd[x],z); hd[x]=ct;} ll num(int siz,int x){return x*(m-x)+max(0,(siz-x)*(n-m-siz+x));} void dp(int x,int fa) { siz[x]=1; for(int i=hd[x],u;i;i=ed[i].nxt) { if((u=ed[i].to)==fa)continue; dp(u,x); siz[x]+=siz[u]; for(int j=min(siz[x],m);j>=0;j--)//x中黑点数 for(int k=max(0,j-(siz[x]-siz[u]));k<=siz[u]&&k<=j;k++)//u中黑点数 f[x][j]=max(f[x][j],f[x][j-k]+f[u][k]+ed[i].w*num(siz[u],k)); // printf("f[%d][%d]=%d f[%d][%d]=%d num(%d,%d)=%lld\n", // u,k,f[u][k],x,j,f[x][j],siz[u],k,num(siz[u],k)); } } int main() { freopen("coloration.in","r",stdin); freopen("coloration.out","w",stdout); scanf("%d%d",&n,&m); for(int i=1,x,y,z;i<n;i++) { scanf("%d%d%d",&x,&y,&z); add(x,y,z); add(y,x,z); } int x=1; siz[x]=1; for(int i=hd[x],u;i;i=ed[i].nxt) { dp(u=ed[i].to,x); siz[x]+=siz[u]; for(int j=min(siz[x],m);j>=0;j--)//x中黑点数 for(int k=max(0,j-(siz[x]-siz[u]));k<=siz[u]&&k<=j;k++)//u中黑点数 f[x][j]=max(f[x][j],f[x][j-k]+f[u][k]+ed[i].w*num(siz[u],k)); // printf("f[%d][%d]=%d f[%d][%d]=%d num(%d,%d)=%lld\n", // u,k,f[u][k],x,j,f[x][j],siz[u],k,num(siz[u],k)); } printf("%lld\n",f[x][m]); return 0; }
T3:给定一张 n*m 的网格图,从左上(1,1)到右下(n,m),有一些格子是障碍格子,从起点格子的中心出发,向右上/右下/左上/左下发射一条光线,光线碰到障碍平面会反射,碰到障碍角会弹回来,碰到边界或边角同理,现在给出障碍坐标、起点坐标和初始方向,问最多能穿过多少个格子。(n,m,k <= 100000)
又是一脸不可做的T3...只会模拟啊...一格一格走什么的...
然而没时间写了,于是有了下面这个没过样例的爆0模拟:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; int const maxn=1e5+5; int n,m,k,sx,sy,xx[4]={-1,-1,1,1},yy[4]={1,-1,1,-1}; int h[4]={2,3,0,1},s[4]={1,0,3,2},op[4]={3,2,1,0},tp[1005][1005]; ll ans; bool vis[1005][1005],b[1005][1005]; bool ck(int x,int y){return (x<1||y<1||x>n||y>m);} bool dfs(int x,int y,int t) { if(vis[x][y]&&(tp[x][y]==t||tp[x][y]==op[t]))return 0; if(!vis[x][y])ans++; vis[x][y]=1; int nx=x+xx[t],ny=y+yy[t]; int tx=x+xx[t],ty=y+yy[t]; if(b[nx][ny]) { nx=x,ny=y; if((b[tx][ny]&&b[nx][ty])||(b[tx][ny]&&ck(nx,ty)) ||(ck(tx,ny)&&b[nx][ty])||(ck(tx,ny)&&ck(nx,ty)) ||(!b[tx]&&!b[ty]))return 1; else if(b[tx][ny])return dfs(tx,ny,h[t]); else return dfs(nx,ty,s[t]); } else { if(((nx>n||nx<1)&&(ty>m||ty<1))||((tx>n||tx<1)&&(ny>m||ny<1)))return 1; else if(nx>n||nx<1)return dfs(nx,ty,h[t]); else if(ny>m||ny<1)return dfs(tx,ny,s[t]); else return dfs(nx,ny,t); } } int main() { freopen("ray.in","r",stdin); freopen("ray.out","w",stdout); scanf("%d%d%d",&n,&m,&k); for(int i=1,x,y;i<=k;i++){scanf("%d%d",&x,&y); b[x][y]=1;} char ch[5]; bool f=0; scanf("%d%d",&sx,&sy); cin>>ch; int fx; if(ch[0]=='N'&&ch[1]=='E')fx=0; if(ch[0]=='N'&&ch[1]=='W')fx=1; if(ch[0]=='S'&&ch[1]=='E')fx=2; if(ch[0]=='S'&&ch[1]=='W')fx=3; f=dfs(sx,sy,fx); if(f)dfs(sx+xx[op[fx]],sy+yy[op[fx]],op[fx]); printf("%d\n",ans); return 0; }
(模拟都好难写啊...)
其实正解就是模拟,因为我们可以发现,能使光线发生改变的格子是 n 级别的,边界是 2*n , k 是 n;
所以模拟有保证,但也不能朴素地一格一格模拟;
因为光线在发生方向改变之前都不会改变路径,所以模拟的单位不是一格,而是一次方向改变;
讲题的同学(dalao)给出了“切比雪夫距离”的定义,大概就是横竖的格子斜着来看,横坐标变成 x+y,纵坐标变成 x-y;(???)
那么一条斜着的光线就成了横平竖直的啦!可以把新坐标放进 vector 里,到时二分查找撞上的会是哪一个障碍;
然而这个好难写啊,使用 vector 本来已经是简化了代码,但是好难写啊!
观摩那位同学(dalao)的代码,真是太优美了!对着它看了一晚上,才总算勉强理解,抄了一遍;
首先,我的习惯本来是 n 行 m 列,坐标的第一个是从上往下数行,第二个是从左往右数列;
可是那位同学(dalao)却和数学课上的平面直角坐标系一样从左往右数坐标的第一个,从下往上数坐标的第二个,为了读代码(TJ)方便,我也在这里暂时改变一下习惯;
把新坐标的横、纵坐标分别用两个 vector 存起来,再附带一个原横坐标,找的时候会方便一点;
方向用两个变量存,变方向的时候 *-1 就好了,很方便;
没想到因为对 vector 的自带函数不熟悉,读 TJ 都费了好大的劲...
有令人惊恐(且十分精妙)的细节,写了许多注释,这里就不再写了;(理解了一晚上)
真无法想象在考场上写出这样的代码,需要多加回顾啊...
改后(AC)代码如下:
#include<iostream> #include<cstdio> #include<cstring> #include<vector> #include<algorithm> #include<cmath> using namespace std; int const maxn=1e5+5; int n,m,k,sx,sy,sdx,sdy,x,y,dx,dy; bool f; long long ans; struct N{ int x,y; N(int x=0,int y=0):x(x),y(y) {} bool operator < (const N &b) const { if(x==b.x)return y<b.y; else return x<b.x; } }; vector<N>v[3]; void add(int x,int y) { v[0].push_back(N(x-y,x));//纵 v[1].push_back(N(x+y,x));//横 } void work() { bool fl=(dx!=dy);//fl=1横走 N p=fl?N(x+y,x):N(x-y,x); vector<N>::iterator it=upper_bound(v[fl].begin(),v[fl].end(),p);//第一个大于p.x的 (精确到了原横坐标!!!) for(;it->x!=p.x;it--);//新坐标相等(且原横坐标第一个大于的) //必须有这句,因为可能原本的y是相同x中最大的,于是就找到了x大于的地方 //上面的情况仅在dx<0时存在(退一步从障碍处出发时退到边缘),所以下面再处理一下到下一个障碍位置 if(dx<0)//横坐标-- for(;it->y>=x;it--);//原横坐标第一个小于x的 ans+=abs(it->y-x)-1;//横看的经过格子数 (it是撞上的障碍,而且从障碍处出发,算下来一共-1) x=it->y; y=fl?(it->x-x):(x-it->x);//下一步横坐标是it带的原横坐标 //从新横坐标中得到当前的纵坐标 // bool u=binary_search(v[0].begin(),v[0].end(),N((x-dx)-y,x-dx)), // q=binary_search(v[0].begin(),v[0].end(),N(x-(y-dy),x)); //二者皆可 bool u=binary_search(v[1].begin(),v[1].end(),N((x-dx)+y,x-dx)), q=binary_search(v[1].begin(),v[1].end(),N(x+(y-dy),x)); //二者皆可 if(u==q)f=1,dx*=-1,dy*=-1; else if(u)x-=dx,dy*=-1;//退一步从障碍处出发 else if(q)y-=dy,dx*=-1; } int main() { freopen("ray.in","r",stdin); freopen("ray.out","w",stdout); scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=m;i++)add(0,i),add(n+1,i); for(int i=0;i<=n+1;i++)add(i,0),add(i,m+1);//注意要包围起来!(网格图) for(int i=1,x,y;i<=k;i++){scanf("%d%d",&x,&y); add(x,y);} sort(v[0].begin(),v[0].end()); sort(v[1].begin(),v[1].end()); char ch[5]; scanf("%d%d",&x,&y); cin>>ch; dx=(ch[0]=='N')?-1:1; dy=(ch[1]=='W')?-1:1; work(); sx=x; sy=y; sdx=dx; sdy=dy;//换起点,为了使它最后在起点处能停一下 ans=0; while(1) { work(); if(x==sx&&y==sy&&dx==sdx&&dy==sdy)break; } if(f)ans>>=1;//原路返回则/2 printf("%lld\n",ans); return 0; }
总结:细节复杂(其实也没有)的T1,思细极恐(就是大模拟)的T3,要怎样才能在考场上写出来...
考虑问题还需要更周密一点,写模拟也要讲究艺术啊。