把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【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_{1\sim n}\),并令\(s_{1\sim n}\)为其前缀和。

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

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

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

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

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

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

\[(i-p)\times (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\times n\)的网格图,你要摆上至少一个\(1\times 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_{1\sim n}\),每个位置可以填上\(1\sim n\),让你求出有多少种方案使得所有\(a_i\le a_{i+1}\),且在其中任选\(k\)个数(\(1\le k\le 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\)使得前\(\lfloor\frac{n+1}2\rfloor\)个位置满足条件,就能使得整个序列满足条件。

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

\[\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 \]

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

  • \(k\le\lfloor\frac{n+1}2\rfloor\)时,由于\(a_k\le a_{n-k+2}\),所以上述式子随着\(k\)增大只可能逐渐变小(或不变)。
  • 由于\(a_i\ge 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(t\ge k)\),就得到转移方程:

\[f_{i,j,k}=\sum_{t\ge k}f_{i-1,j+k,t}\times(t-k+1)=\sum_{t\ge k}f_{i-1,j+k,t}\times t-\sum_{t\ge k}f_{i-1,j+k,t}\times (k-1) \]

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

然后注意若\(n-1\)是奇数,对于\(f_{\lfloor\frac{n+1}2\rfloor,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_i\ge0\),求\(\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_{1\sim i}\ge p_i\),求\(\prod_{x=2}^i(p_{x-1}-p_x+1)\)的和。(问题②)

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

满足\(p'_{1\sim 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\times(i-1)}\)转移过来。

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

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

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

\(p_{x-1}-p_x+1\),其实能看作是在\(p_x\sim 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\)

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

因此我们同样倒枚每一条边,对于每条电线记录\(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\)(这也是\(n\le2\)时无解的原因),所以若每次我们都从\(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
}
posted @ 2020-05-15 08:18  TheLostWeak  阅读(239)  评论(0编辑  收藏  举报