【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\)的个数为:
我们考虑求出把第\(p\sim i-1\)个数全都刚好加到\(a_i+m\),如果加不到或恰好加满说明当前答案可行,若有\(1\)剩余说明不可行。
那么需要多少个\(1\)呢?这时候前面记下的前缀和就派上用场了,需要的\(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-k+1}\)刚好是相对的,因此,就能得到一个重要结论(这个结论在正解中也非常有用):
我们只需\(DP\)使得前\(\lfloor\frac{n+1}2\rfloor\)个位置满足条件,就能使得整个序列满足条件。
而我们考虑如果不用前缀和表示,这个式子本应是:
然后从这个式子我们可以发现两点:
- 在\(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}\)和\(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
}