cf 1561(div2)
比赛链接: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(n\sqrt{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\n",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\n",i,k,p,ret); ans[i]=((ll)ans[i]+(ll)ans[k-1]*(ret-p)%m)%m; ret=p; cnt++; } //printf("ans[%d]=%d\n",i,ans[i]); sum=(sum+ans[i])%m; if(i==2000000)printf("cnt=%d\n",cnt); } printf("%d\n",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\n",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\n",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\n",m); for(int i=1;i<=m;i++) printf("%d%c",ans[i],(i==m)?'\n':' '); } return 0; }