(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)的个数为:
我们考虑求出把第(psim 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 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-k+1})刚好是相对的,因此,就能得到一个重要结论(这个结论在正解中也非常有用):
我们只需(DP)使得前(lfloorfrac{n+1}2 floor)个位置满足条件,就能使得整个序列满足条件。
而我们考虑如果不用前缀和表示,这个式子本应是:
然后从这个式子我们可以发现两点:
- 在(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})和(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
}