比赛链接:http://codeforces.com/contest/1561
前三题照例很简单(虽然C题WA了两次)。做出了D1。
D
题意:
对于一个数(n),每次可以减(1—(n-1))中的一个数,或者除以(2—n)中的一个数,问有多少种方式变成(1)。
分析:
从(x)位置跳到1,我们考虑第一步跳什么。第一步可以减某个数,所以答案是前面所有答案的和;第一步也可以除某个数,答案加上除到的位置的答案。
除到的位置是一段一段的,比如(x=6)的时候是:
(1(6/6), 1(6/5), 1(6/4), 2(6/3), 3(6/2) )
我们要跳着取,相同的一段直接算。
其实这里要严谨地考虑时间复杂度的话,应该是分成(<sqrt{x})和(>sqrt{x})两部分。时间复杂度(O(nsqrt{n}))。但都跳也能过。
代码如下:
#include<iostream> #include<ctime> #define ll long long using namespace std; int const N=4e6+5; int n,m,ans[N]; int main() { double st=clock(); scanf("%d%d",&n,&m); ans[1]=1; ans[2]=2; ll sum=3; for(int i=3;i<=n;i++) { ans[i]=sum; int ret=i; int cnt=0; //printf("i=%d sum=%lld ",i,sum); for(int k=2;k<i&&ret>1;k=i/(i/k)+1) { int p=i/k; //printf("i=%d k=%d p=%d ret=%d ",i,k,p,ret); ans[i]=((ll)ans[i]+(ll)ans[k-1]*(ret-p)%m)%m; ret=p; cnt++; } //printf("ans[%d]=%d ",i,ans[i]); sum=(sum+ans[i])%m; if(i==2000000)printf("cnt=%d ",cnt); } printf("%d ",ans[n]); double ed=clock(); //cout<<(ed-st)/CLOCKS_PER_SEC<<endl; return 0; }
D2和D1的唯一不同就是(n)的范围变成(4*10^6)了,所以不能再用上面的做法。比赛时我就开始干瞪眼了。
实际上,这里有个“关键观察”:考虑(x)和(x-1)能除到的地方有什么不同。先上几个例子:
(2: 1)
(3: 1, 1)
(4: 1, 1, 2)
(5: 1, 1, 1, 2)
(6: 1, 1, 1, 2, 3)
观察一番,再结合一些思考,可以发现(x)能除到的地方和(x-1)相比,首先是多了一个(1)((x/x = 1))。其次,对于(x)的一个因数(p)(不包括(1)和(x)),它会把(x-1)能除到的位置中的一个(p-1)换成(p)。仔细想想,确实是这样。
所以一种做法可以是记录每个(x)可以除到的这些位置的答案之和(f[x])。计算(x)的答案时,枚举(x)的每个因子,把(f[x-1])更新成(f[x]),再计算当前的答案。
然而枚举因子还是不够优秀。我们不妨从因子出发,直接考虑它对它的倍数的影响。
也就是再记录一个(ad[x])。计算出当前(i)的答案(ans[i]),需要枚举(i)的所有倍数(j),(ad[j]=ad[j]-ans[i-1]+ans[i])。于是(f[x] = f[x-1] + ad[x] )。
这样做时间复杂度(O(nlnn)),空间复杂度(O(n))。
代码如下:
#include<iostream> #define ll long long using namespace std; int const N=4e6+5; int n,m,f[N],ad[N],ans[N]; int mo(ll x){x%=m; if(x<0)x+=m; return x;} int main() { scanf("%d%d",&n,&m); int sum=0; ans[1]=1; for(int i=2;i<=n;i++)ad[i]=1;//i/i->1 for(int i=2;i<=n;i++) { f[i]=mo(f[i-1]+ad[i]); ans[i]=mo((ll)sum+1+f[i]); //printf("i=%d sum=%d f=%d ans=%d ",i,sum,f[i],ans[i]); for(int j=2*i;j<=n;j+=i) ad[j]=mo((ll)ad[j]-ans[i-1]+ans[i]); sum=mo((ll)sum+ans[i]); } printf("%d ",ans[n]); return 0; }
E
题意:
给一个奇数长度的排列,每次只能翻转长度为奇数的一个前缀,问能否在(frac{5n}{2})次内将其排序,并给出方案。
分析:
首先,由于翻转的是奇数长度的前缀,所以奇数位置的数只能移动到奇数位置,偶数位置的数只能移动到偶数位置。如果有(a[i])和(i)的奇偶性不同,那么无解。
否则一定有解,只要按下面的方法:
首先,我们把(n)和(n-1)移动到数列末尾,就可以不再管它们了。
这启发我们每次把最后两个数排好——这只需要五步:(假设当前要排的是(nw)和(nw-1))
1.找到(nw)所在的位置(x)(奇数),翻转(1—x),此时(nw)在第一个位置;
2.找到(nw-1)所在的位置(y)(偶数),翻转(1—(y-1)),此时(nw)在(nw-1)前一个位置;
3.翻转(1—(y+1)),此时(nw)在第三个位置,(nw-1)在第二个位置;
4.翻转(1—3),此时(nw)在第一个位置,(nw-1)在第二个位置;
5.翻转(1—nw),此时(nw)和(nw-1)就排好了。
所以最多五步排好两个数,最终答案不超过(frac{5n}{2})。
好娱乐啊,这个题。
代码如下:
#include<iostream> using namespace std; int const N=2022; int T,n,a[N],ans[N*5]; void rev(int l,int r) { int mid=((l+r)>>1); for(int i=l,j=r;i<mid;i++,j--)swap(a[i],a[j]); } int main() { scanf("%d",&T); while(T--) { scanf("%d",&n); bool fl=1; for(int i=1;i<=n;i++) { scanf("%d",&a[i]); if((a[i]&1)!=(i&1))fl=0; } if(!fl){puts("-1"); continue;} int m=0,nw=n,x,y; while(nw>1) { if(a[nw]==nw&&a[nw-1]==nw-1){nw-=2; continue;} for(int i=1;i<=n;i++) if(a[i]==nw){x=i; break;} if(x>1){ans[++m]=x; rev(1,x);} for(int i=1;i<=n;i++) if(a[i]==nw-1){y=i; break;} if(y-1>1){ans[++m]=y-1; rev(1,y-1);} ans[++m]=y+1; rev(1,y+1); ans[++m]=3; rev(1,3); ans[++m]=nw; rev(1,nw); nw-=2; } printf("%d ",m); for(int i=1;i<=m;i++) printf("%d%c",ans[i],(i==m)?' ':' '); } return 0; }