第十五届中北大学算法与程序设计竞赛
比赛地址:传过去送的门
好久没写博客了,没想到突然间就松懈了那么久,得紧起来了。
之前的随笔里的代码因为设置了行内标号,现在复制代码前面都带上了行号,而且没法点击复制,哪位能人异士,信男信女懂怎么觉得,please help help me。
话不多说,水了场比赛,再来水个题解。比赛时就做了9个,DJLM以及正经写法等补了再补上。
A.俄罗斯方块
上来先开的这个,好家伙,带图的,应该挺难。然后看玩题目,好吧,直接模拟就好,从最上面判断到下面,找到当前方块的底层,然后把它放上就行。
#include<bits/stdc++.h> using namespace std; int mp[21][21]; int main(){ int n,op,x; scanf("%d",&n); for(int i=0;i<=10;i++) mp[0][i]=1; while(n--){ scanf("%d%d",&op,&x); if(op==1){ for(int i=10;i>=1;i--){ if(mp[i-1][x]||mp[i-1][x+1]){ mp[i][x]=mp[i][x+1]=mp[i+1][x]=mp[i+1][x+1]=1; break; } } }else if(op==2){ for(int i=10;i>=1;i--){ if(mp[i-1][x]||mp[i-1][x+1]||mp[i-1][x+2]){ mp[i][x]=mp[i][x+1]=mp[i][x+2]=mp[i+1][x]=1; break; } } }else if(op==3){ for(int i=10;i>=1;i--){ if(mp[i-1][x]||mp[i-1][x+1]||mp[i-1][x+2]||mp[i-1][x+3]){ mp[i][x]=mp[i][x+1]=mp[i][x+2]=mp[i][x+3]=1; break; } } }else{ for(int i=10;i>=1;i--){ if(mp[i-1][x]||mp[i-1][x+1]||mp[i-1][x+2]){ mp[i][x]=mp[i][x+1]=mp[i][x+2]=mp[i+1][x+1]=1; break; } } } } for(int i=10;i>=1;i--) for(int j=1;j<=10;j++) printf("%d%c",mp[i][j]," \n"[j==10]); return 0; }
B.真的是签到题
真的就是签到题。
K.签个到
贪心。贪心往往是种直觉,很难解释里面的哲学。
我这里瞎比比一下我的想法,我们假设,修改之后最大值跟最小值的位置是x,y,那么未修改的差值就是a[x]-a[y],接下来每次修改便是使得差值增大x或者增大y。
我们要差值最大,所以m次修改自然是都作用在下标最大的位置最好。所以先求出,a[i]+m*i中的最大值和a[i]-m*i中的最小值,然后再分别与原a[i]做差,取绝对值最大值。
还有注意n==1的时候。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+11; ll a[N]; int main(){ int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); ll mx=a[1]+m,mi=a[1]-m,ans=0; for(int i=2;i<=n;i++){ mx=max(mx,a[i]+1ll*i*m); mi=min(mi,a[i]-1ll*i*m); } for(int i=1;i<=n;i++){ ans=max(ans,abs(mx-a[i])); ans=max(ans,abs(mi-a[i])); } if(n==1) printf("0\n"); else printf("%lld\n",ans); return 0; }
E.简单的线性代数
说到线代就得想起军波,好吧军波老师跟这题没关。
因为操作的都是n阶方阵,那他们之间的乘除其实也就跟数的乘除差不多,都满足结合律啊等等的。
设B=E+A+..+Ak-1 ,根据等比数列求和公式就有B=E(E-Ak)/(E-A)。
而求的是B的逆,也就是B-1,那就是E-A=E(E-Ak)B-1。
题目又说了Ak=0.所以B-1=E-A,也就是A矩阵主对角线上的数被1减去,其余的被0减去。(看样例输出,也可以猜一发,搏一搏,转眼就开学。)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1011; ll a[N][N]; int main(){ int n; ll k; scanf("%d%lld",&n,&k); for(int i=0;i<n;i++) for(int j=0;j<n;j++) scanf("%lld",&a[i][j]); for(int i=0;i<n;i++) for(int j=0;j<n;j++){ if(i==j) a[i][j]=1ll-a[i][j]; else a[i][j]*=-1ll; printf("%lld%c",a[i][j]," \n"[j==n-1]); } return 0; }
G.数位操作1
这题应该算个模拟题?,首先啊,ans>9,那么对于n<=9的答案肯定是10+n。
对于n>10的呢,因为要把n拆成一堆<=9的数的乘积,那如果n包含了>9的质因子,此时肯定表示不粗来了,就是-1。
对于可以表示出来的n,我们就把它里面的2,3,5,7拆出来,考虑到要ans最小,首先肯定要先保证数位最少。
5跟7的话,肯定占一个数位了,而3个2可以合成一个8只占1个数位,而2个3可以合成一个9只占一个数位。
然后我们需要对多出来的2跟3,另外讨论,如果两个2,一个3的话,此时2可以合成4占一个数位,3也要单独占一个数位,但我们可以把4中的一个2给3,这时26比34要小。
1个2和1个3话,他们可以合成6只占一个数位。
再多出来的2,3就直接是2,3了,最后我们再把所有数排序就好了。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const ll a[11]={2,3,5,7}; int cnt[11],ans[111]; int main(){ ll n; while(~scanf("%lld",&n)){ if(n<10){ printf("%lld\n",10+n); continue; } for(int i=0;i<4;i++){ cnt[i]=0; while(n%a[i]==0){ n/=a[i]; cnt[i]++; } } if(n>1) printf("-1\n"); else{ int pn=0; while(cnt[0]>=3){ ans[pn++]=8; cnt[0]-=3; } while(cnt[1]>=2){ ans[pn++]=9; cnt[1]-=2; } if(cnt[0]==2){ if(cnt[1]){ cnt[1]--; ans[pn++]=2; ans[pn++]=6; }else ans[pn++]=4; } else if(cnt[0]==1){ if(cnt[1]){ cnt[1]--; ans[pn++]=6; }else ans[pn++]=2; } if(cnt[1]==1) ans[pn++]=3; while(cnt[2]){ ans[pn++]=5; cnt[2]--; } while(cnt[3]){ ans[pn++]=7; cnt[3]--; } sort(ans,ans+pn); for(int i=0;i<pn;i++) printf("%d",ans[i]); printf("\n"); } } return 0; }
C.港口
跟着榜做,看这题才十几个人过,以为是很难的题,点开一看,似曾相似燕归来?
这是道原题,具体题号忘了,记得那题还需要求最小操作下有多少相同的种数。
回到这题,这题的思想是差分,不知道差分是什么的小问号们可以看我的差分系列博客(绝对没有骗点击量)。
我们求出差分数组dif的话,根据题意,我们就是要让dif都为0,此时就是大家都一样了。
而一段区间[l,r]内加上一个数a的话,便是在差分数组上的体现就是dif[l]+a,dif[r+1]-a。
所以,我们要最少操作次数的话,每次肯定是选取dif[l]跟dif[r]一正一负的来进行一个操作。
而多次来的那些正的或者是负的dif,则让每次操作l=0,或者r=n+1,也就是从开始到这个位置,或者这个位置到结尾的区间都加上a即可。
这题里a是1,所以答案就是max(所有正的dif和,所有负的dif的绝对值和)。
而多说一点,相同的种数的话便是所有dif的总和的绝对值+1,因为1个是最终相同的,而它可以更改的范围就是多出来的dif进行搭配,由此不改变方案数。
#include<bits/stdc++.h> using namespace std; const int N=1e5+11; typedef long long ll; ll a[N]; int main(){ int n; scanf("%d",&n); for(int i=0;i<n;i++) scanf("%lld",&a[i]); ll ans1=0,ans2=0; for(int i=1;i<n;i++){ if(a[i]>=a[i-1]) ans1+=a[i]-a[i-1]; else ans2-=a[i]-a[i-1]; } printf("%lld\n",max(ans1,ans2)); return 0; }
F.集合操作.
也是跟着榜做,看这题才几个人过,以为是很难的题。但想了会,set瞎搞就行了。
如果,我们有个答案数组存了所有的数,当1操作其实是在这个数组中删去x,而2操作是加上x,而3操作的话就是二分找到第一个大于等于x的数。
但x<1000000000,直接存所有数是不可能的。而想一下,每个x,如果它不在题目的S集合中,那么答案肯定就是x,否则就得看前面有没有往S中添加过x+1。
而添加过x+1的话,就还得看x+2,一直到一个没有操作中出现过的数,而这个数肯定是操作出现过的某个数+1。
所有对于每次操作,我们把x跟x+1添加进我们前面所说的答案数组中,这个数组的也就2*n而已了。
为了方便直接用set来实现这个答案数组即可。因为set操作大概是log的,所以nlogn时间复杂度上也可以。
#include<bits/stdc++.h> #include<tr1/unordered_map> using namespace std; const int N=1e6+11; int op[N],x[N]; set<int> se; int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d%d",&op[i],&x[i]); se.insert(x[i]); se.insert(x[i]+1); } for(int i=1;i<=n;i++){ if(op[i]==1){ if(se.count(x[i])) se.erase(x[i]); }else if(op[i]==2) se.insert(x[i]); else{ set<int>::iterator it=se.lower_bound(x[i]); int ans=*it; printf("%d\n",ans); } } return 0; }
H.数位操作2
看上去像个数位dp。刚想往那里想一下,突然想到直接递推dp可不可以呢。
dp[i][sum][p1][p2]即为前i个位置,数的和为sum,倒数第二个数位p1,倒数第一个数位p2的方案数。
转移的话,假设下一个数为p3,先判断(p1*100+p2*10+p3)%x等不等于0,然后便是
dp[i+1][sum+p3][p2][p3]+=dp[i][sum][p1][p2]
这样需要遍历i,然后枚举sum,以及p1,p2,p3,时间复杂度大概是50*450*9*9*9=18,225,000,1e7级别的,而2秒刚好可以。
#include<bits/stdc++.h> #include<tr1/unordered_map> using namespace std; const int N=55,M=511,md=1000009; int dp[N][M][11][11]; int main(){ int n,sum,x; scanf("%d%d%d",&n,&sum,&x); if(sum>450){ printf("0\n"); return 0; } // int res=0; // for(int i=0;i<=9;i++) // for(int j=0;j<=9;j++) // for(int k=0;k<=9;k++){ // if(i+j+k==sum&&(i*100+j*10+k)%x==0) res++; // } // printf("%d\n",res); for(int i=0;i<=9;i++) for(int j=0;j<=9;j++) dp[2][i+j][i][j]=1; for(int i=2;i<n;i++){ int lim=min(9*i,sum); for(int j=0;j<=lim;j++) for(int k=0;k<=9;k++) for(int l=0;l<=9;l++) for(int m=0;m<=9;m++){ if((k*100+l*10+m)%x!=0) continue; dp[i+1][j+m][l][m]=(dp[i+1][j+m][l][m]+dp[i][j][k][l])%md; } } int ans=0; for(int i=0;i<=9;i++) for(int j=0;j<=9;j++){ ans=(ans+dp[n][sum][i][j])%md; } printf("%d\n",ans); return 0; }
I.配对
图论,好久没做了。最大对数不用说了,每个奇数都可跟每个偶数配对,也就是两者数目中的最小值。
那把奇数分一边,偶数分一边,老二分图了,价值和也就是要求二分图最大权匹配?km算法都快忘了(其实已经忘了。)
看数据量比较小,所以我尝试的是最小费用最大流的做法。
我们用个源点跟所有奇数建边,流量为1,费用为0,而所有偶数跟汇点建边,流量为1,费用为0。而奇数与偶数建边,流量为1,费用变为负的两者的异或值。
建好边,跑一遍最小费用最大流完事,为了避免超时,我用的是势函数的dijk的版本的费用流。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=611; const ll inf=1e16+7; struct Side{ int v,ne,w,val; Side(){} Side(int v,int val):v(v),val(val){} bool operator<(const Side &s1)const{ return val>s1.val; } }S[N*N]; bool vis[N]; int sb,se,sn,a[N],head[N],lu[N],h[N]; ll dis[N],flow[N]; void init(int n){ sn=0; for(int i=0;i<=n;i++) head[i]=-1; } void add(int u,int v,int w,int val){ S[sn].w=w;S[sn].val=val; S[sn].v=v;S[sn].ne=head[u]; head[u]=sn++; } void addE(int u,int v,int w,int val){ add(u,v,w,val);add(v,u,0,-val); } bool dijk(int n){ priority_queue<Side> q; for(int i=0;i<=n;i++){ lu[i]=-1; dis[i]=flow[i]=inf; } dis[sb]=0; q.push(Side(sb,dis[sb])); while(!q.empty()){ int u=q.top().v,w=q.top().val;q.pop(); if(dis[u]<w) continue; for(int i=head[u];~i;i=S[i].ne){ int v=S[i].v; if(S[i].w>0&&dis[v]>dis[u]+S[i].val+h[u]-h[v]){ lu[v]=i; dis[v]=dis[u]+S[i].val+h[u]-h[v]; flow[v]=min(flow[u],1ll*S[i].w); q.push(Side(v,dis[v])); } } } return dis[se]!=inf; } int mfml(int n){ int ans=0; ll ansc=0; for(int i=0;i<=n;i++) h[i]=0; while(dijk(n)){ ans+=flow[se]; ansc+=flow[se]*(dis[se]+h[se]-h[sb]); for(int i=lu[se];~i;i=lu[S[i^1].v]){ S[i].w-=flow[se]; S[i^1].w+=flow[se]; } for(int i=0;i<=n;i++) h[i]+=dis[i]; } printf("%d %lld\n",ans,-ansc); return 0; } int main(){ int n; scanf("%d",&n); sb=0;se=n+1; init(n+1); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); if(a[i]&1) addE(sb,i,1,0); else addE(i,se,1,0); for(int j=1;j<i;j++){ if((a[i]+a[j])&1){ if(a[i]&1) addE(i,j,1,-(a[i]^a[j])); else addE(j,i,1,-(a[i]^a[j])); } } } mfml(n+1); return 0; }
这回我没咕咕咕。
D.构造数组
这题也不是什么难题,就直接从后往前看,如果当前这个数要插入的位置是pos,那么它真实位置肯定是要大于等于pos的,因为在它后面的数插在比pos小的位置,会把它往后挤。
所以就是用个线段树来维护[1,n]区间每个位置有没有数,然后就查询第一个存在着pos个空位的位置。
#include<bits/stdc++.h> #define L(x) (x<<1) #define R(x) (x<<1|1) #define M(x) ((T[x].l+T[x].r)>>1) using namespace std; const int N=5e5+11; struct Node{ int l,r,pos; }T[N<<2]; int ind[N],num[N],ans[N]; void build(int x,int l,int r){ T[x].l=l;T[x].r=r; if(l==r){ T[x].pos=1; return ; } build(L(x),l,M(x)); build(R(x),M(x)+1,r); T[x].pos=T[L(x)].pos+T[R(x)].pos; } int modify(int x,int y){ if(T[x].l==T[x].r){ T[x].pos=0; return T[x].l; } int ans; if(T[L(x)].pos>=y) ans=modify(L(x),y); else ans=modify(R(x),y-T[L(x)].pos); T[x].pos=T[L(x)].pos+T[R(x)].pos;; return ans; } int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d%d",&ind[i],&num[i]); build(1,1,n); for(int i=n;i>=1;i--){ int p=modify(1,ind[i]); ans[p]=num[i]; } for(int i=1;i<=n;i++) printf("%d%c",ans[i]," \n"[i==n]); return 0; }
M.降维打击
每次都可以上下左右随意走的话,总的方案数是4k,那么就需要找到能走到边界的方案数。
这里就可以以走的次数来进行dp递推,或者是bfs来进行。我这里是写bfs,dp[x][y][z]就是到x,y位置时已经走了z步的方案数。因为队列里是肯定是按走的步数少的在前面的,所以每个位置同样的z只进队一次即可。
bfs完了之后,我们就统计边界位置能在0~k内走到的方案数。注意的是,如果这个边界位置在kk步就已经走到了,那么剩下的4k-kk的方案数也算是走到了边界的。
#include<bits/stdc++.h> using namespace std; const int N=131,md=1000000007; const int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}}; struct Node{ int x,y,z; Node(){} Node(int x,int y,int z):x(x),y(y),z(z){} }; queue<Node> q; int n,m,k,x,y,dp[N][N][N*2]; bool mp[N][N],vis[N][N][N*2]; void bfs(){ dp[x][y][0]=1; vis[x][y][0]=1; q.push(Node(x,y,0)); while(!q.empty()){ Node p=q.front(); q.pop(); if(p.z==k) continue; for(int i=0;i<4;i++){ int dx=p.x+dir[i][0]; int dy=p.y+dir[i][1]; if(dx<0||dx>n+1||dy<0||dy>m+1) continue; if(mp[dx][dy]) continue; dp[dx][dy][p.z+1]+=dp[p.x][p.y][p.z]; if(dp[dx][dy][p.z+1]>=md) dp[dx][dy][p.z+1]-=md; if(dx<1||dx>n||dy<1||dy>m) continue; if(vis[dx][dy][p.z+1]) continue; vis[dx][dy][p.z+1]=1; q.push(Node(dx,dy,p.z+1)); } } } int poww(int a,int b){ int ans=1; while(b){ if(b&1) ans=1ll*ans*a%md; a=1ll*a*a%md; b>>=1; } return ans; } int main(){ scanf("%d%d%d%d%d",&n,&m,&x,&y,&k); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&mp[i][j]); bfs(); int fz=0,fm=poww(4,k); for(int kk=0;kk<=k;kk++){ for(int i=0;i<=n+1;i++){ fz+=1ll*dp[i][0][kk]*poww(4,k-kk)%md; if(fz>=md) fz-=md; fz+=1ll*dp[i][m+1][kk]*poww(4,k-kk)%md; if(fz>=md) fz-=md; } } for(int kk=0;kk<=k;kk++){ for(int i=1;i<=m;i++){ fz+=1ll*dp[0][i][kk]*poww(4,k-kk)%md; if(fz>=md) fz-=md; fz+=1ll*dp[n+1][i][kk]*poww(4,k-kk)%md; if(fz>=md) fz-=md; } } fm=poww(fm,md-2); fz=1ll*fz*fm%md; printf("%d\n",fz); return 0; }
L.20200524
一看就是容斥了,可我tm数学不好。。。
我们先把20200524的因子提出出来,接下来就是它们之间的匹配。比如x中2的倍数,跟y中20200524/2的倍数进行配对。
但是像x中2的倍数与y中20200524/2的倍数进行配对,然后x中6的倍数与y中20200524/6的倍数进行配对时,前者会包含后者的方案数,产生了重复。
因为2的倍数中包含了6的倍数,所以算x中2的倍数与y中20200524/2的倍数时,应该去掉x/中6的倍数与y中20200524/2的倍数进行配对的方案数。
那么我们先预处理出,每个20200524的因子在x中的倍数的个数,然后就可以根据容斥,在某个因子的方案数中把该因子的倍数的方案数去掉。最后再与y中的进行匹对。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int n=20200524,m=24; const int a[31]={1,2,3,4,6,12,1123,1499,2246,2998,3369,4492,4497,5996,6738,8994,13476, 17988,1683377,3366754,5050131,6733508,10100262,20200524}; int b[31]; int main(){ int t,x,y; scanf("%d",&t); while(t--){ scanf("%d%d",&x,&y); for(int i=0;i<m;i++) b[i]=x/a[i]; for(int i=m-1;i>=0;i--) for(int j=i+1;j<m;j++) if(a[j]%a[i]==0) b[i]-=b[j]; ll ans=0; for(int i=0;i<m;i++) ans+=1ll*b[i]*(y/(n/a[i])); printf("%lld\n",ans); } return 0; }
J.异或Tree
看这个树上,还有在线更新的,除了树链剖分想不到其他的了,但是区间异或和不会处理,hhhhh,艹了,去看一下大佬怎么写的先了。
区间异或和的和
水完了,溜溜溜。