• 【AtCoder】AtCoder Grand Contest 041 解题报告


    点此进入比赛

    (A):Table Tennis Training(点此看题面

    大致题意:(n)个位置,一开始一对好朋友分别在(a,b)两个位置。每单位时间,二人可分别选择向左或向右走(1)个位置(若超出范围则保持不动),求二人至少要多少时间相遇。

    不妨设(a<b),则可以进行如下分类讨论。

    (b-a)为偶数

    显然,此时二人只要向着对方走,只需(frac{b-a}2)的时间即可相遇。

    (b-a)为奇数

    显然,此时必然是一个人走到(1)(n),在那里等一回合使得二人间距离为偶数,然后再走到一起。

    而且必然是(a)走到(1)或者(b)走到(n),因此我们再分类讨论:

    • (a)走到(1)(a)走到(1)并等待一回合共需要(a)的时间,此时(b)走到了(b-a),因此还需(frac{b-a-1}2)的时间。总时间为(a+frac{b-a-1}2)
    • (b)走到(n)(b)走到(n)并等待一回合共需要(n-b+1)的时间,此时(a)走到了(n-b+1+a),因此还需(frac{b-a-1}2)的时间。总时间为((n-b+1)+frac{b-a+1}2)

    综上所述,最短时间应为(min(a,n-b+1)+frac{b-a+1}2)

    提交记录

    神奇地(CE)了两发。。。(似乎是选错了语言,好像不能选Clang?)

    然后一发就过了。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define LL long long
    #define swap(x,y) (x^=y^=x^=y)
    #define min(x,y) ((x)<(y)?(x):(y))
    using namespace std;
    LL n,a,b;
    int main()
    {
    	scanf("%lld%lld%lld",&n,&a,&b);if(a>b&&swap(a,b),(a&1)==(b&1)) return printf("%lld",b-a>>1),0;//使a<b;处理偶数情况
    	return printf("%lld",(b-a-1>>1)+min(a,n-b+1)),0;//处理奇数情况
    }
    

    (B):Voting Judges(点此看题面

    大致题意:(n)个数,你需要进行(m)次操作,每次选择恰好(v)个数各加(1)。求有多少个数在(m)次操作后可能成为前(p)大的数。

    可二分性≠要用二分做

    显然这道题是具有可二分性的,因为如果一个数可以,那么比它大的数一定都可以。

    但我们一定要用二分吗?事实上,我们直接枚举判断,也可以做到(O(n))

    因此,具有可二分性的题目,不一定就要用二分做。

    如何判断

    考虑将所有数从大到小排序,设为(a_{1sim n}),并令(s_{1sim n})为其前缀和。

    假设我们当前判断(a_i)是否可能成为前(p)大的数,那么按照贪心的思想,我们只要让它刚好卡在第(p)个就可以了。

    因此,我们可以把第(i)个数、最大的(p-1)个数(因为这(p-1)个数是无须超越的)、比(i)小的(n-i+1)个数(因为这些数怎么加都超不过第(i)个数)全都加上(m)

    则还剩下需要加的(1)的个数为:

    [max(v-p-(n-i),0) imes m ]

    我们考虑求出把第(psim i-1)个数全都刚好加到(a_i+m),如果加不到或恰好加满说明当前答案可行,若有(1)剩余说明不可行。

    那么需要多少个(1)呢?这时候前面记下的前缀和就派上用场了,需要的(1)的个数就是:

    [(i-p) imes (a_i+m)-(s_{i-1}-s_{p-1}) ]

    似乎我们只要比较这个式子与前面式子的大小关系就能得出答案了,但这其实是有瑕疵的。

    为什么呢?因为假如你有一个数原本就大于(a_i+m),难道你还能给它负数个(1),让它变小吗?

    所以,我们要先判一下(a_i+m)是否大于等于(a_p),然后再进行上述判断。(闪指导就被这个坑了)

    提交记录

    第一发交完等待评测的时候,突然发现有个地方漏开(long long)了。

    于是第一发果不其然(WA)了,开了(long long)重交了一发才过。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 100000
    #define LL long long
    using namespace std;
    int n,m,v,p,a[N+5];LL s[N+5];
    I bool cmp(CI x,CI y) {return x>y;}//从大到小排序
    int main()
    {
    	RI i;for(scanf("%d%d%d%d",&n,&m,&v,&p),i=1;i<=n;++i) scanf("%d",a+i);
    	for(sort(a+1,a+n+1,cmp),i=1;i<=n;++i) s[i]=s[i-1]+a[i];//排序后求前缀和
    	for(i=n;i>p;--i) if(a[i]+m>=a[p])//枚举答案,首先要使得加上m后大于等于第p个数
    		if(1LL*max(v-p-(n-i),0)*m<=1LL*(a[i]+m)*(i-p)-(s[i-1]-s[p-1])) break;//用1去填平中间的坑
    	return printf("%d",i),0;//输出答案(若始终没break则最后i恰好等于p)
    }
    

    (C):Domino Quality(点此看题面

    大致题意: 有一张(n imes n)的网格图,你要摆上至少一个(1 imes 2)的骨牌,使得各行各列覆盖其至少一格的骨牌个数皆相等。

    注意:接下来开始的三部分都只是我的思路历程(勿喷),此题终题解只需看总结部分即可。

    手玩一小时

    这种构造题一般套路想想都是手玩->发现规律->切掉,怀着这样的想法,我就开始了暗无天日的手玩。。。

    于是,大约一小时过去了,弱小的我和强大的闪指导依旧只画出了(n=3,4,5)的解。

    而且,没有任何规律。。。

    题解的辅助

    没办法,只好去找了篇题解,看了看最终的规律似乎是:

    • 对于(n=3,5,7),直接打表。
    • 对于(n)为偶数,若(n=4)则打表,否则规律显然。
    • 对于(n)为奇数,补一个(n=5)的答案转化为(n)为偶数。

    于是我们从中得到了几点启发:

    • 难怪我们没有找到任何规律,因为我们求出的这几个答案都是特殊情况。。。
    • 我们还需要手玩出(n=7)并发现(n)为偶数的规律,再去考虑怎么补上(n=5)的答案把奇数变成偶数,这又将是一个浩大的工程。

    画去画去就困了,先睡个觉,明天再接着画。

    自习课加成

    果然,正如闪指导所言,在班里就能智商++,自习课更是有着极为优秀的加成。

    第二天起来到班里,来机房前就无聊乱画(说起来今明两天是月考,因为停课逃掉了),结果突然就把(n)为偶数情况的规律画出来了?!

    此时极为亢奋的我再接再厉,不到一分钟又画出了(n=7)?!

    (顺便说一下,过于亢奋的我刚画完就兴冲冲地奔向了机房,到了之后发现画了图的草稿纸没带,结果凭借残余的记忆还又用了五六分钟才重新画出(n=7)的解。由此再次验证了闪指导的话的正确性,果然闪指导是我们的红太阳,他说的话都是真理!)

    总结

    好了,上面那一堆都是废话,接下来才是正经的结论。

    • 对于(n=3,5,7),直接打表。(可见代码)

    • 对于(n)为偶数,我的确是自己推出了一个统一的规律,但感觉把(n)分类为模(6)(0,2,4)三种情况似乎更好理解、说明,也更好码(码量虽然大了点,但实际只是多按了几个(Ctrl C+Ctrl V)):

      (n\%6=0)如图所示:(由于我比较懒,图被吞了,直接用题目中的表示方法吧)

      x.aa......z.
      x.bb......z.
      y.cc......y.
      y...aa....y.
      z...bb....x.
      z...cc....x.
      .x....aa...z
      .x....bb...z
      .y....cc...y
      .y......aa.y
      .z......bb.x
      .z......cc.x
      

      我想图画出来后这规律应该就比较显然了吧。。。

      (n\%6=2)如图所示

      x.aa.......p..
      x.bb.......p..
      y..cc.......z.
      y...aa......z.
      z...bb......y.
      z....cc.....y.
      .x....aa....x.
      .x....bb....x.
      .y.....cc....z
      .y......aa...z
      .z......bb...y
      .z.......cc..y
      ..p.......aa.x
      ..p.......bb.x
      

      (n\%6=0)很像,不是吗?事实上,仔细观察就发现仅仅是把所有"cc"右移了一格(可理解为"pp"的存在占据了"cc"原本的位置)。

      (n\%6=4)如图所示

      x.aa.........q..
      x..bb........q..
      y..cc........p..
      y...aa.......p..
      z....bb.......z.
      z....cc.......z.
      .x....aa......y.
      .x.....bb.....y.
      .y.....cc.....x.
      .y......aa....x.
      .z.......bb....z
      .z.......cc....z
      ..p.......aa...y
      ..p........bb..y
      ..q........cc..x
      ..q.........aa.x
      

      看完前两种情况就很好明白了,"pp"和"qq"占据了"bb"和"cc"原本的位置,使得"bb"和"cc"需要右移一格。

    • 对于(n)为奇数,我们需要这样画:

      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      **********.....
      ..........abba.
      ..........a..ab
      ..........bba.b
      ..........a.acc
      ..........abbaa
      

      其中"*"构成的矩阵表示(n-5)的答案,右下角为(5)的答案。

    大体上就是这样了,然后就是模拟模拟模拟。

    提交记录

    (RE)???仔细看了一遍代码,然后发现智障地写了这样一句:

    return puts("-1");
    

    (WA)???而且只(WA)一个点???

    不断思索有什么单个数据是特殊的,最后发现挂在了(9)上((5+4)),(9)中的(4)也是需要特判的,而我原本是把(4)单独拉出来特判的。所以只好又把(4)的情况放回了偶数大家庭。。。

    然后终于过了。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 1000
    using namespace std;
    int n,a[N+5];char s[N+5][N+5];
    char s3[4][4]={"","aab","b.b","baa"};
    char s5[6][6]={"","abba.","a..ab","bba.b","a.acc","abbaa"};
    char s7[8][8]={"","aabbaa.","b..a..a","b..a..a","...baab","...bccb","abb...a","acc...a"};
    char s4[5][5]={"","aaba","ccba","abcc","abaa"};
    I void Draw(CI n)//模拟看起来很长,实际上仔细看下就发现三部分几乎一样,只有小变动而已(实际上我本来就基本上是复制粘贴的)
    {
    	RI i,j;if(n==4)//特判n=4
    	{
    		for(i=1;i<=4;++i) for(j=1;j<=4;++j) s[i][j]=s4[i][j-1];return;
    	}
    	if(n%6==0)
    	{
    		for(i=1;i<=n/6;++i)
    			s[6*i-5][i]=s[6*i-4][i]=s[n-6*i+6][n-i+1]=s[n-6*i+5][n-i+1]='x',
    			s[6*i-3][i]=s[6*i-2][i]=s[n-6*i+4][n-i+1]=s[n-6*i+3][n-i+1]='y',
    			s[6*i-1][i]=s[6*i][i]=s[n-6*i+2][n-i+1]=s[n-6*i+1][n-i+1]='z';
    		for(i=1;i<=n/3;++i)
    			s[3*i-2][n/6+2*i-1]=s[3*i-2][n/6+2*i]='a',
    			s[3*i-1][n/6+2*i-1]=s[3*i-1][n/6+2*i]='b',
    			s[3*i][n/6+2*i-1]=s[3*i][n/6+2*i]='c';
    	}
    	if(n%6==2)
    	{
    		for(i=1;i<=n/6;++i)
    			s[6*i-5][i]=s[6*i-4][i]=s[n-6*i+6][n-i+1]=s[n-6*i+5][n-i+1]='x',
    			s[6*i-3][i]=s[6*i-2][i]=s[n-6*i+4][n-i+1]=s[n-6*i+3][n-i+1]='y',
    			s[6*i-1][i]=s[6*i][i]=s[n-6*i+2][n-i+1]=s[n-6*i+1][n-i+1]='z';
    		s[n-1][i]=s[n][i]=s[2][n-i+1]=s[1][n-i+1]='p';
    		for(i=1;i<=n/3;++i)
    			s[3*i-2][n/6+2*i-1]=s[3*i-2][n/6+2*i]='a',
    			s[3*i-1][n/6+2*i-1]=s[3*i-1][n/6+2*i]='b',
    			s[3*i][n/6+2*i]=s[3*i][n/6+2*i+1]='c';
    		s[n-1][n/6+2*i-1]=s[n-1][n/6+2*i]='a',
    		s[n][n/6+2*i-1]=s[n][n/6+2*i]='b';
    	}
    	if(n%6==4)
    	{
    		for(i=1;i<=n/6;++i)
    			s[6*i-5][i]=s[6*i-4][i]=s[n-6*i+6][n-i+1]=s[n-6*i+5][n-i+1]='x',
    			s[6*i-3][i]=s[6*i-2][i]=s[n-6*i+4][n-i+1]=s[n-6*i+3][n-i+1]='y',
    			s[6*i-1][i]=s[6*i][i]=s[n-6*i+2][n-i+1]=s[n-6*i+1][n-i+1]='z';
    		s[n-3][i]=s[n-2][i]=s[4][n-i+1]=s[3][n-i+1]='p',
    		s[n-1][i]=s[n][i]=s[2][n-i+1]=s[1][n-i+1]='q';
    		for(i=1;i<=n/3;++i)
    			s[3*i-2][n/6+2*i-1]=s[3*i-2][n/6+2*i]='a',
    			s[3*i-1][n/6+2*i]=s[3*i-1][n/6+2*i+1]='b',
    			s[3*i][n/6+2*i]=s[3*i][n/6+2*i+1]='c';
    		s[n][n/6+2*i-1]=s[n][n/6+2*i]='a';
    	}
    }
    int main()
    {
    	RI i,j;if(scanf("%d",&n),n==2) return puts("-1"),0;//n=2输出-1
    	if(n==3) {for(i=1;i<=3;++i) puts(s3[i]);return 0;}//特判n=3
    	if(n==5) {for(i=1;i<=5;++i) puts(s5[i]);return 0;}//特判n=5
    	if(n==7) {for(i=1;i<=7;++i) puts(s7[i]);return 0;}//特判n=7
    	for(i=1;i<=n;++i) for(j=1;j<=n;++j) s[i][j]='.';//初始化点阵
    	if(n&1) for(Draw(n-5),i=1;i<=5;++i) for(j=1;j<=5;++j) s[n-5+i][n-5+j]=s5[i][j-1];else Draw(n);//奇数转化为偶数,然后开始模拟画画
    	for(i=1;i<=n;++i) puts(s[i]+1);return 0;//输出答案
    }
    

    (D):Problem Scores(点此看题面

    大致题意:(n)个位置(a_{1sim n}),每个位置可以填上(1sim n),让你求出有多少种方案使得所有(a_ile a_{i+1}),且在其中任选(k)个数((1le kle n))之和都一定大于任选出的(k-1)个数之和。

    暴力(DP)

    像我这种菜鸡也就只能推推暴力,因此这里先讲讲我的暴力想法吧(并非裸暴力),或许能对正解有一点启发。

    考虑任选(k)个数大于任选(k-1)个数,显然只要满足最小的(k)个数之和大于最大的(k-1)个数之和即可。

    (S_i=sum_{i=1}^na_i),则这个限制用式子表示就是:

    [S_k>S_n-S_{n-k+1}Leftrightarrow S_k+S_{n-k+1}>S_n ]

    然后我们发现(S_k)(S_{n-k+1})刚好是相对的,因此,就能得到一个重要结论(这个结论在正解中也非常有用):

    我们只需(DP)使得前(lfloorfrac{n+1}2 floor)个位置满足条件,就能使得整个序列满足条件。

    而我们考虑如果不用前缀和表示,这个式子本应是:

    [sum_{i=1}^ka_i>sum_{i=1}^{k-1}a_{n-i+1}Leftrightarrow a_1-sum_{i=2}^k(a_{n-i+2}-a_i)>0 ]

    然后从这个式子我们可以发现两点:

    • (klelfloorfrac{n+1}2 floor)时,由于(a_kle a_{n-k+2}),所以上述式子随着(k)增大只可能逐渐变小(或不变)。
    • 由于(a_ige a_{i-1},a_{n-i+2}le a_{n-i+1}),所以(a_{n-i+2}-a_i)同样是递减(或不变)的。

    于是就有一个很朴素的想法了:

    (f_{i,j,k})表示前(i)个数,当前的总和(a_1-sum_{i=2}^k(a_{n-i+2}-a_i))(j),当前的差值(a_{n-i+2}-a_i)(k)的方案数,注意此处的(k)和题目里的(k)定义重了(习惯使然)。

    考虑转移,不难发现上次的总和必然是(j+k),则我们枚举上次的差值(t(tge k)),就得到转移方程:

    [f_{i,j,k}=sum_{tge k}f_{i-1,j+k,t} imes(t-k+1)=sum_{tge k}f_{i-1,j+k,t} imes t-sum_{tge k}f_{i-1,j+k,t} imes (k-1) ]

    如果每次我们维护(f_{i,j,k})(f_{i,j,k} imes k)两个后缀和,即可实现(O(1))转移了。

    然后注意若(n-1)是奇数,对于(f_{lfloorfrac{n+1}2 floor,j,k})的答案,中间单独的这一个数有(k+1)种选择方式,因此统计答案时还需乘上一个系数。(具体实现详见代码)

    因此这个暴力总复杂度是(O(n^3))的,不过闪指导说因为我卡了一下枚举上界,根据调和级数复杂度大概是(O(n^2logn))

    这里先贴一份暴力代码吧:

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 5000
    using namespace std;
    int n,X,f[N+5][N+5],g[N+5][N+5],gk[N+5][N+5];
    int main()
    {
    	RI i,j,k;scanf("%d%d",&n,&X);
    	for(j=1;j<=n;++j) for(f[j][n-j]=1,k=n-j;~k;--k) g[j][k]=1,gk[j][k]=n-j;//预处理
    	RI hn=(n+1)/2;for(i=2;i<=hn;++i)//枚举i
    	{
    		for(j=1;j<=n;++j) for(k=(n-j)/i;~k;--k) f[j][k]=(gk[j+k][k]-1LL*g[j+k][k]*(k-1)%X+X)%X;//状态转移
    		for(j=1;j<=n;++j)//求后缀和
    		{
    			g[j][(n-j)/i]=f[j][(n-j)/i],gk[j][(n-j)/i]=1LL*f[j][(n-j)/i]*((n-j)/i)%X;//单独处理最高位,注意这么写是为了避免清数组
    			for(k=((n-j)/i)-1;~k;--k) g[j][k]=(f[j][k]+g[j][k+1])%X,gk[j][k]=(1LL*f[j][k]*k+gk[j][k+1])%X;//从高到低后缀和
    		}
    	}
    	RI ans=0;for(j=1;j<=n;++j) for(k=(n-j)/hn;~k;--k) ans=(1LL*((n-1)&1?k+1:1)*f[j][k]+ans)%X;//统计答案,注意n-1为奇数时的系数
    	return printf("%d",ans),0;
    }
    

    正解

    我们假设(p_i=a_{n-i+2}-a_i)。特殊地,(p_1=n-a_1)

    于是原问题就变成了,(sum_{i=1}^kp_i< n)(p_{i-1}ge p_ige0),求(prod_{x=2}^i(p_{x-1}-p_x+1))的和。(问题①)

    (实质上,(sum_{i=1}^kp_i)就是前面的(n-j)(p_{x-1}-p_x+1)就是前面转移时的系数(t-k+1)

    然后我们考虑之前是三维(DP),而看数据范围就知道正解应该是(O(n^2))的,于是就要考虑如何去掉一维,或者说,应该考虑究竟去掉哪一维。

    我和闪指导都一直觉得应该是去掉和的那一维,因为差的那一维要作为转移系数。然而现实总是如此打脸(这也体现出我永远都只能写暴力的必然性),最后去掉的居然是差的那一维!

    为什么呢?它又是如何去掉的呢?

    让我们来看一下吧,考虑我们当前枚举(p_i),而(p_i)必然小于等于之前所有(p),即当前的问题为:

    满足(p_{1sim i}ge p_i),求(prod_{x=2}^i(p_{x-1}-p_x+1))的和。(问题②)

    如果我们就给所有数减去(p_i),设新的数列为(p'),显然所有数都减去同一个数是不会影响差值的,所以现在问题②就变成了:

    满足(p'_{1sim i}ge 0),求(prod_{x=2}^i(p'_{x-1}-p'_x+1))的和。(问题③)

    此时(p')的取值范围与问题①中的(p)是相同,故可以看作是问题①的一个小问题。

    所以如果我们设(f_{i,j})表示对于前(i)(p),它们的总和等于(j)(prod_{x=2}^i(p_{x-1}-p_x+1))的和,那么只要枚举一个(k)表示当前的(p_i)(f_{i,j})就可以从之前的(f_{i-1,j-k imes(i-1)})转移过来。

    而统计答案时同样需要枚举(p_{lfloorfrac{n+1}2 floor})来计算。

    好了,说了这么多,我们发现,尽管减少了一维,但复杂度似乎依然没变,仍旧是(O(n^3))的。。。(这个暴力我就懒得码了)

    但实际上,对于这个(DP),我们可以进一步优化。

    (p_{x-1}-p_x+1),其实能看作是在(p_xsim p_{x-1})之中选出一个关键点的方案数。

    而如果我们不去枚举(k),而是用(0/1)的一维来表示是否选择过关键点,然后转移,就可以实现(O(n^2))了(具体实现可以详见代码,比暴力还简短)。

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 5000
    #define Inc(x,y) ((x+=(y))>=X&&(x-=X))
    using namespace std;
    int n,X,f[N+5][N+5][2];
    int main()
    {
    	RI i,j;scanf("%d%d",&n,&X),f[1][0][0]=1;//初始化
    	RI hn=(n+1)/2;for(i=2;i<=hn;++i) for(j=0;j^n;++j)//枚举i,j
    		f[i][j][1]=f[i-1][j][0],j-i+1>=0&&Inc(f[i][j][1],f[i][j-i+1][1]),//对于1的转移
    		f[i][j][0]=f[i][j][1],j-i+1>=0&&Inc(f[i][j][0],f[i][j-i+1][0]);//对于0的转移
    	RI ans=0;for(i=1;i<=n;++i) for(j=(n-i)/hn;~j;--j)//统计答案,这里i,j和之前定义不太一样
    		ans=(1LL*((n-1)&1?j+1:1)*f[hn][n-i-j*hn][0]+ans)%X;//同样需要乘上系数,j枚举p[hn]
    	return printf("%d",ans),0;
    }
    

    (E):Balancing Network(点此看题面

    大致题意:(n)条电线和(m)条有相对顺序的边,每条边连接两条电线。当沿着电线走时,每遇上一条向外连的边,就需要转移到对应电线上。你需要给每条边确定方向,对于两种任务分别使得从任意电线出发最终都会走到同一电线上、从不同电线出发可能走到不同电线上。

    (E)题似乎比(D)题简单?思路又清晰,代码又好写。。。

    对于(T=1)

    (\%\%\%) (hl666)秒了此题。

    我们可以对每条电线开一个(bitset)(设为(S[x])),来记录从哪些电线出发最终可能到达这条电线。

    显然对于第(i)条边就是让(S[a_i]=S[b_i]=S[a_i]|S[b_i])

    然后我们枚举每一条电线,找到一条所有点都能到达的电线,考虑如何构造方案。

    正着搞似乎比较不可做,于是我们就倒枚每一条边。

    对于每条电线开一个(p_x)记录是否能走到目标电线,然后对于一条边,就向(p_x=1)的那条电线连。(若(p_x)皆为(0)则随便连,反正这种情况下这条边肯定是走不到了)

    对于(T=2)

    实际上,这种情况只有(nle2)时可能会无解。

    因此我们同样倒枚每一条边,对于每条电线记录(t_x)表示当前看来会走到哪一条电线。

    如果我们从(u)(v)连了一条边,那么(t_u)就会变成(t_v)(这应该是显然的)。

    而考虑我们的任务其实很简单,只要让所有(t_x)不全相等即可。

    那么我们令(k_w)表示(t_x=w)(x)的个数,则最终任务就是不让任何一个(k_w)变成(n)

    因为当(n>2)时,不会出现某一时刻两个不同的(t_x)所对应的(k)皆为(n-1)(这也是(nle2)时无解的原因),所以若每次我们都从(k)大的向(k)小的连边,就无论如何也不可能出现(k)变成(n)的情况。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 50000
    #define M 100000
    using namespace std;
    int n,m,op,a[M+5],b[M+5];char ans[M+5];
    class UniformingSolver//对于T=1
    {
    	private:
    		int p[N+5];bitset<N+5> S[N+5];
    	public:
    		I void Solve()
    		{
    			RI i,j;for(i=1;i<=n;++i) S[i].set(i);//初始化bitset
    			for(i=1;i<=m;++i) S[a[i]]=S[b[i]]=S[a[i]]|S[b[i]];//对于每条边合并bitset
    			for(i=1;i<=n;++i) if(S[i].count()==n)//找到一条目标电线
    			{
    				for(p[i]=1,j=m;j;--j) p[a[j]]?//判断每条边连向
    					(p[b[j]]=1,ans[j]='^'):(p[a[j]]=p[b[j]],ans[j]='v');
    				puts(ans+1);return;//输出答案
    			}puts("-1");//没有目标电线,输出-1
    		}
    }S1;
    class NonUniformingSolver//对于T=2
    {
    	private:
    		int t[N+5],k[N+5];
    	public:
    		I void Solve()
    		{
    			RI i;if(n<=2) return (void)(puts("-1"));for(i=1;i<=n;++i) k[t[i]=i]=1;//特判无解,然后初始化t,k
    			#define U(x,y) (--k[t[x]],++k[t[x]=t[y]])//把x的目标电线改为y的目标电线
    			for(i=m;i;--i) k[t[a[i]]]<=k[t[b[i]]]?//向k小的连边
    				(ans[i]='^',U(b[i],a[i])):(ans[i]='v',U(a[i],b[i]));
    			puts(ans+1);
    		}
    }S2;
    int main()
    {
    	RI i;for(scanf("%d%d%d",&n,&m,&op),i=1;i<=m;++i) scanf("%d%d",a+i,b+i);
    	return op==1?S1.Solve():S2.Solve(),0;
    }
    

    (F):Histogram Rooks(点此看题面

    大致题意:(n)列格子,其中第(i)列有(a_i)行。一个车可以占据一行一列,问有多少种放车的方式能占据所有的行和列。

    动态规划

    虽然这种数据范围一看就是动态规划,但我这种蒟蒻显然是推不来的,于是又要膜拜(hl666)

    我们考虑对于每一列,共计有三种状态:

    • 这一列上有车。
    • 这一列上没有车,但每一行都被车占据了。
    • 这一列上没有车,且存在行没被车占据掉。

    显然如果仅仅这样依然不容易设计(DP)

    所以这里我们可以枚举每一行进行(DP),则对于上面三种状态的分类,根据当前行之下的那些行是否已经被完全占据,把第二类状态归入第一类(下面行已被完全占据)或是第三类(下面行未被完全占据)。

    也就是说,我们可以多枚举一维状态(h),并用一维(0/1)表示下面的行是否被完全占据。

    注意在枚举(h)时,由于每一列只有(a_i)行,所以列与列之间可能会断开。而断开的列是互不影响的,因此我们可以把它们看作若干段连通的行与列进一步讨论。

    而具体的(DP)转移可以直接详见代码。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define N 400
    #define X 998244353
    using namespace std;
    int n,cnt,a[N+5],P2[N+5],C[N+5][N+5],id[N+5][N+5],f[2*N+5][N+5][N+5][2],g[2*N+5];
    I int DP(CI l,CI r,CI h)//这道题中的DP更适宜采用记忆化搜索的形式
    {
    	if(l>r) return 0;RI k=(id[l][r]?id[l][r]:id[l][r]=++cnt);
    	//记录当前区间的编号,因为区间个数实际上是O(n)规模的,从而防止MLE
    	RI i,j;for(i=l;i<=r;++i) if(a[i]==h) break;if(i<=r)//如果找到某一列行数等于h,说明列与列之间断开
    	{
    		RI x=DP(l,i-1,h),y=DP(i+1,r,h);g[k]=g[x]+g[y]+1;//g记录列数
    		for(i=0;i<=g[x];++i) for(j=0;j<=g[y];++j)//在两个连通块中分别枚举转移
    			f[k][h][i+j][0]=(1LL*f[x][h][i][0]*f[y][h][j][0]+f[k][h][i+j][0])%X,
    			f[k][h][i+j+1][1]=(1LL*f[x][h][i][1]*f[y][h][j][1]+f[k][h][i+j+1][1])%X;
    	}
    	else
    	{
    		for(DP(l,r,h+1),i=0;i<=g[k];++i)//先处理更高一行,然后处理这一行
    			f[k][h][i][0]=f[k][h+1][i][1],f[k][h][i][1]=f[k][h+1][i][1],//从上一行直接转移下来
    			f[k][h][i][0]=(1LL*f[k][h+1][i][0]*(P2[g[k]-i]-1)+f[k][h][i][0])%X,//占据这一行,可以随便填,但不能不填
    			f[k][h][i][1]=(1LL*f[k][h+1][i][1]*(P2[g[k]-i]-1)+f[k][h][i][1])%X;
    		for(i=1;i<=g[k];++i) for(j=1;j<=i;++j)//选出若干列,剩余列可以随便填,注意转移系数要乘上组合数
    			f[k][h][i-j][0]=(1LL*P2[g[k]-i]*C[i][j]%X*f[k][h+1][i][0]+f[k][h][i-j][0])%X,
    			f[k][h][i-j][1]=(1LL*P2[g[k]-i]*C[i][j]%X*f[k][h+1][i][1]+f[k][h][i-j][1])%X;
    	}return k;
    }
    int main()
    {
    	RI i,j,Mx=0;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i),Mx<a[i]&&(Mx=a[i]);
    	for(i=1;i<=Mx+1;++i) f[0][i][0][0]=f[0][i][0][1]=1;//初始化
    	for(P2[0]=i=1;i<=n;++i) P2[i]=(P2[i-1]<<1)%X;//预处理2的幂
    	for(C[0][0]=i=1;i<=n;++i) for(C[i][0]=j=1;j<=i;++j) C[i][j]=(C[i-1][j-1]+C[i-1][j])%X;//预处理组合数
    	return printf("%d",f[DP(1,n,0)][0][0][0]),0;//DP
    }
    
  • 相关阅读:
    《深度学习推荐系统中各类流行的Embedding方法(上)》
    《替代梯度下降——基于极大值原理的深度学习训练算法》&《加速对抗训练——YOPO算法浅析》
    《【炼丹技巧】功守道:NLP中的对抗训练 + PyTorch实现》
    《论文阅读:对抗训练(adversarial training)》
    《一文搞懂NLP中的对抗训练》
    CLOUD配置审批流发消息
    CLOUD财务成本核算
    K3CLOUDJOBPROCESS每分钟重启
    查看服务器日志
    数据规则列表加导入导出
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/AtCoderAGC041.html
Copyright © 2020-2023  润新知