2.22 contest
题解
A题
给出一个无向图,若(a,b,c)(a < b < c),a和b有边,a和c有边,则b和c有边。求染色方案数
n,m <= 3e5
这是一个弦图。
答案 = ∏(n - G[v]).G[v]是和v相邻的标号比v大的节点个数,显然这些点构成一个团
每对点(u,v)之间连边当且仅当存在一条路径u --> v且路径上每个点的标号小于u和v。
我们从小到大遍历,求出只用前i个点构成的联通块。并用一个set记录每个联通块外与之相邻的点数,和i有边且标号大于i的点是i所在联通块的set中所有元素。
每次加入一个点,启发式合并set
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,int> pr;
const int inf = 1e9;
const int maxn = 300000 + 20;
const ll mod = 998244353;
struct node{
int next,to;
}e[maxn * 2];
int head[maxn],cnt;
int n,m,fa[maxn];
set <int> s[maxn];
inline void adde(int x,int y){
e[++cnt].to = y;
e[cnt].next = head[x];
head[x] = cnt;
}
inline int getfa(int x){ return x == fa[x] ? x : fa[x] = getfa(fa[x]); }
inline void merge(int x,int y){
if ( s[x].size() < s[y].size() ) swap(x,y);
fa[y] = x;
for (auto it = s[y].begin() ; it != s[y].end() ; ++it) s[x].insert(*it);
}
void insert(int x){
fore(i,x){
if ( e[i].to > x ) continue;
int p = getfa(x) , q = getfa(e[i].to);
if ( p != q ) merge(p,q);
}
}
int main(){
scanf("%d %d",&n,&m);
rep(i,1,m){
int x,y;
scanf("%d %d",&x,&y);
adde(x,y) , adde(y,x);
}
rep(i,1,n){
fa[i] = i;
fore(j,i){
if ( e[j].to < i ) continue;
s[i].insert(e[j].to);
}
}
ll ans = 1;
rep(i,1,n){
insert(i);
int p = getfa(i);
while ( s[p].size() && *s[p].begin() <= i ) s[p].erase(s[p].begin());
ans = ans * (n - s[p].size()) % mod;
}
cout<<ans<<endl;
}
B题
给出一个括号序列,每个括号是蓝,灰,绿三种颜色。翻转一些括号使得只留下蓝和灰,只留下蓝和绿的括号序都合法,问最小翻转多少个括号 n <= 6000
括号序列问题把“(”看成+1,“)”看成-1,合法括号序列即任意处前缀和>=0,且和等于0
因为蓝色很特殊。考虑枚举蓝色的和,假设为b。则另外两种颜色的和是-b。
并且考虑翻转括号序列使得其和等于给定值,肯定翻两端最优。
然后我们把剩下的操作看成交换一对同色的括号(这时不能改变任意一种颜色的和)。显然我们交换最靠端点的一对是最优的。
现在只考虑蓝和灰两种颜色的括号序列。枚举每个不合法位置。假设和为-z
我们翻转蓝色最多可以贡献2 * x,翻转灰色最多贡献2 * y,那么得到一个不等式
m
i
n
(
x
,
b
)
+
m
i
n
(
y
,
l
)
>
=
(
−
z
+
1
)
/
2
min(x,b) + min(y,l) >= (-z + 1) / 2
min(x,b)+min(y,l)>=(−z+1)/2 b和l表示蓝色,灰色的翻转次数。把min拆开得到四个不等式,最后可以得到关于b,l,g的几个不等式,大概长这样:
l
>
=
c
1
,
b
>
=
c
2
,
g
>
=
c
3
,
l
+
b
>
=
c
4
,
g
+
b
>
=
c
5
l>=c1 , b >= c2 , g >= c3 , l + b >= c4 , g + b >= c5
l>=c1,b>=c2,g>=c3,l+b>=c4,g+b>=c5 我们要最小化l + b + g,那么b取尽量大
这个过程大佬wxh说可以用线段树维护(每种颜色对每个位置的最多贡献有分界点),感觉细节很多
总结:
括号序列问题把“(”看成+1,“)”看成-1,合法括号序列即任意处前缀和>=0,且和等于0
固定左右括号的个数。再贪心的选取翻转
写题的时候真的必须确认每个细节!因为以为不用判"("的不合法情况。调了一个小时。其实漏掉这种情况会漏判不合法
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,int> pr;
const int inf = 1e9;
const int maxn = 6000 + 20;
const ll mod = 998244353;
char ch[maxn];
int n,c[maxn];
int b[maxn],l[maxn],g[maxn],sumb,suml,sumg,cntb,cntl,cntg;
int curb[maxn],curl[maxn],curg[maxn],sufb[maxn],sufl[maxn],sufg[maxn],preb[maxn],prel[maxn],preg[maxn];
int flag,cur,s[maxn];
inline void keep_bal(int a[],int sum){
if ( sum > 0 ){
rep(i,1,n){
if ( a[i] == -1 ) a[i] = 1 , sum -= 2;
if ( sum <= 0 ) return;
}
}
else if ( sum < 0 ){
repd(i,n,1){
if ( a[i] == 1 ) a[i] = -1 , sum += 2;
if ( sum >= 0 ) return;
}
}
}
int find_ans(){
rep(i,1,n){
preb[i] = preb[i - 1] + (curb[i] == -1);
prel[i] = prel[i - 1] + (curl[i] == -1);
preg[i] = preg[i - 1] + (curg[i] == -1);
}
repd(i,n,1){
sufb[i] = sufb[i + 1] + (curb[i] == 1);
sufl[i] = sufl[i + 1] + (curl[i] == 1);
sufg[i] = sufg[i + 1] + (curg[i] == 1);
}
int mxl = 0 , mxg = 0 , mxb = 0 , mxlb = 0 , mxgb = 0;
//blue - lime
rep(i,1,n){
s[i] = s[i - 1] + curb[i] + curl[i];
if ( s[i] < 0 ){ // 在“(”也要考虑,有可能不合法
int y = min(preb[i],sufb[i + 1]), x = min(prel[i],sufl[i + 1]) , z = (-s[i] + 1) / 2;
mxl = max(mxl,z - y) , mxlb = max(mxlb,z) , mxb = max(mxb,z - x);
if ( x + y < z ){ flag = 0; return 0; }
}
}
//blue - grey
rep(i,1,n){
s[i] = s[i - 1] + curb[i] + curg[i];
if ( s[i] < 0 ){
int y = min(preb[i],sufb[i + 1]), x = min(preg[i],sufg[i + 1]) , z = (-s[i] + 1) / 2;
mxg = max(mxg,z - y) , mxgb = max(mxgb,z) , mxb = max(mxb,z - x);
if ( x + y < z ){ flag = 0; return 0; }
}
}
return mxl + mxg + max(mxb,max(mxlb - mxl,mxgb - mxg));
}
int main(){
// freopen("input.txt","r",stdin);
scanf("%d",&n);
scanf("%s",ch + 1);
rep(i,1,n){
scanf("%d",&c[i]);
int t = ch[i] == '(' ? 1 : -1;
if ( c[i] == 0 ) l[i] = t , suml += t , cntl++;
else if ( c[i] == 1 ) g[i] = t , sumg += t , cntg++;
else b[i] = t , sumb += t , cntb++;
}
int ans = inf;
rep(i,-n,n){
if ( ((-i - sumg) & 1) || ((i - sumb) & 1) || ((-i - suml) & 1) ) continue;
if ( abs(i) > min(cntb,min(cntl,cntg)) ) continue;
int cur = (abs(i - sumb) + abs(-i - suml) + abs(-i - sumg)) / 2; flag = 1;
if ( cur >= ans ) continue;
rep(i,1,n) curb[i] = b[i] , curl[i] = l[i] , curg[i] = g[i];
keep_bal(curb,i - sumb);
keep_bal(curl,-i - suml);
keep_bal(curg,-i - sumg);
cur += find_ans() * 2;
if ( flag ) ans = min(ans,cur);
// cout<<i<<" "<<ans<<endl;
}
if ( ans == inf ) puts("-1");
else printf("%d\n",ans);
}
E题
给出一个序列,每次可以删除一个元素,直至只剩一个元素。删除后不能有两个连续相同数。求方案数
简单的区间DP:
f[i][j]记录区间[i,j]只保留i,j的删除方案数。当a[i] = a[j]时不合法。每次枚举区间中最后被删除数来转移。注意两个区间合并的时删除顺序可以任意排列,要乘组合数
主要是利用这个DP状态的定义来确定边界条件。想清楚再写。
这道题都调了10分钟,非常糟糕!!!
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,int> pr;
const int inf = 1e9;
const int maxn = 520;
const ll mod = 998244353;
ll f[maxn][maxn],fac[maxn],inv[maxn];
int a[maxn],n;
inline void up(ll &x,ll y){ x = (x + y) % mod; }
inline ll power(ll x,ll y){
ll res = 1;
while ( y ){
if ( y & 1 ) res = res * x % mod;
x = x * x % mod;
y >>= 1;
}
return res;
}
inline ll C(int n,int m){
if ( n < m ) return 0;
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
int main(){
scanf("%d",&n);
rep(i,1,n) scanf("%d",&a[i]);
fac[0] = inv[0] = 1;
rep(i,1,n) fac[i] = fac[i - 1] * i % mod , inv[i] = power(fac[i],mod - 2);
a[n + 1] = n + 1;
rep(i,0,n){
f[i][i] = 1;
if ( a[i] != a[i + 1] ) f[i][i + 1] = 1;
}
rep(len,3,n + 2){
rep(l,0,n){
int r = len + l - 1;
if ( a[l] == a[r] || r > n + 1 ) continue;
rep(k,l + 1,r - 1){
up(f[l][r],f[l][k] * f[k][r] % mod * C(r - l - 2,k - l - 1));
}
}
}
cout<<f[0][n + 1]<<endl;
}
F题
给出两个数组,每次可以将(ai,ai + 1)和(ai + 2k,ai + 2k + 1)互换,问a数组能否转换成b
序列转换和等价问题,要找到做变换后的不变量。然后将变换操作化归到局部,变成通用或者简单的操作。
这道题发现
1.一次交换后逆序对奇偶性不变(奇数位置的改变和偶数位置抵消)。
2.可以一次交换每个元素,直到最后三个元素
于是得出结论(大胆猜测,可以用归纳法证明)。当序列是排列时,能相互转换的充要条件是奇数、偶数位置上的集合对应相等,并且两个序列的奇偶性不同。
如果不是排列,只要奇数或者偶数集合中有相等元素,意味着逆序对可以具有两种奇偶性,相等的元素大小关系可以任意确定,所以肯定有解
再手造几组样例,发现结论正确!
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,int> pr;
const ld inf = 2e18;
const int N = 3e6 + 10;
const int maxn = 300020;
const ll mod = 1e9 + 7;
int num,cnt[maxn];
int cnt1[maxn],cnt2[maxn];
int n;
int a[maxn],b[maxn];
int query(int x){
int res = 0;
fordown(i,x) res += cnt[i];
return res & 1;
}
void modify(int x,int d){
forup(i,x,n) cnt[i] += d;
}
bool check(){
rep(i,1,n){
if ( i & 1 ) cnt1[a[i]]++;
else cnt2[a[i]]++;
}
int flag= 0;
rep(i,1,n){
if ( cnt1[b[i]] > 1 || cnt2[b[i]] > 1 ) flag = 1;
if ( i & 1 ) cnt1[b[i]]--;
else cnt2[b[i]]--;
}
rep(i,1,n) if ( cnt1[i] != 0 || cnt2[i] != 0 ) return 0;
if ( flag ) return 1;
rep(i,1,n){
if ( i & 1 ){
num ^= query(a[i]);
modify(a[i],1);
}
}
rep(i,1,n) cnt[i] = 0;
rep(i,1,n){
if ( !(i & 1) ){
num ^= query(a[i]);
modify(a[i],1);
}
}
rep(i,1,n) cnt[i] = 0;
rep(i,1,n){
if ( i & 1 ){
num ^= query(b[i]);
modify(b[i],1);
}
}
rep(i,1,n) cnt[i] = 0;
rep(i,1,n){
if ( !(i & 1) ){
num ^= query(b[i]);
modify(b[i],1);
}
}
return num ^ 1;
}
int main(){
scanf("%d",&n);
rep(i,1,n) scanf("%d",&a[i]);
rep(i,1,n) scanf("%d",&b[i]);
printf("%d\n",check());
}
H题
题意难以描述
非常好的一道思维题
从不合法的集合出发(当前已知肯定不是)
我们每次可以知道距离确定不合法的集合中任以元素恰好为1的数组不合法(一个元素不同)。有点像bfs的感觉。因为如果a数组距离已知不合法为1,一定会有人确定自己的数不等于那个已知不合法的对应数
接下来问题转化成找给出的a数组和最开始不合法的集合中的元素的最近距离,即相同数的个数最多。
用一个DP,表示强制选第i个点。枚举上一个选的点。发现有点像最长上升子序列那样转移。因为有b,c的限制,看上去需要二维查最大值。其实不需要。因为b,c限制同时满足时下标一定满足(否则无解:即存在b[i] > c[i]).因此舍掉下标这一维。注意,不能保留下标,舍弃b或c的限制,因为b或c的限制比下标更强
总结:
这题的思路非常新颖。从不合法的集合出发,根据题目的特性来bfs,直到找到a数组。这种题一开始完全没有思路。要积累!
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<ll,ll> pr;
const ll inf = 1e18;
const int maxn = 100020;
const ll mod = 998244353;
int n,f[maxn],mx[maxn],tot;
ll a[maxn],b[maxn],c[maxn],x[maxn],y[maxn],dt[maxn];
pr pt[maxn];
bool check(){
rep(i,1,n - 1) if ( b[i] > c[i] ) return 0;
return 1;
}
bool cmp(pr a,pr b){
if ( a.fi == b.fi ) return a.se > b.se;
return a.fi < b.fi;
}
inline int get(ll x){
return lower_bound(y + 1,y + n + 1,x) - y;
}
inline int query(int x){
int res = 0;
fordown(i,x) res = max(res,mx[i]);
return res;
}
inline void modify(int x,int d){
forup(i,x,n) mx[i] = max(mx[i],d);
}
int main(){
scanf("%d",&n);
rep(i,1,n) scanf("%lld",&a[i]);
rep(i,1,n - 1) scanf("%lld",&b[i]);
rep(i,1,n - 1) scanf("%lld",&c[i]);
if ( !check() ) { puts("-1"); return 0; }
rep(i,1,n - 1){
c[i + 1] += c[i] , b[i + 1] += b[i];
}
rep(i,1,n){
x[i] = c[i - 1] - a[i] , y[i] = -b[i - 1] + a[i];
pt[i] = mp(x[i],y[i]);
}
sort(y + 1,y + n + 1);
sort(pt + 1,pt + n + 1);
int ans = 0;
rep(i,1,n){
f[i] = query(get(pt[i].se)) + 1;
modify(get(pt[i].se),f[i]);
// cout<<pt[i].fi<<" "<<get(pt[i].se)<<endl;
ans = max(ans,f[i]);
}
cout<<n - ans<<endl;
}
I题
博弈
把圆转化成点。然后把垃圾桶划分成先手必胜/必败的区域。因为两个点距离>=2 * r
可以做2r的圆弧来确定区域。用几何画板画一下就能够直观的理解了。
发现每个区域,无论是上凸还是上凹,高度都是sqrt(15) / 2 * r
总结:
这类博弈题从策略入手。找到必胜、必败的划分性质
博弈还要多加强
用几何画板很浪费时间,不要轻易使用。
增加自己的直观想象力。从题目的本质理解。
把接近完整的块用eps判掉,防止精读爆炸
#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-5;
int main(){
double h,r,tmp;
cin>>r>>h;
h -= 2 * r , tmp = sqrt(15) / 2 * r;
int t = (int)(h / tmp - eps) + 1;
if ( (t & 1) ) printf("1\n");
else printf("2\n");
}
L题
求所有n个点的带标号无向图,1-2的最短距离和 n <= 400
求最短路最常用的想法是把图分层DP,每一层只能和上一层连边或者层与层之间连边。
然后观察到2-n其实是等价的,只需要求所有点的最短距离和 / (n - 1)
记录贡献和的DP,经常同时记录方案数和当前贡献和。所以不需要记录当前在第几层,贡献随着DP加上。
f[i][j]表示还剩i个点,上一层有j 个点的方案数,g[i][j]表示当前已经考虑过的贡献和
f[i - l][l] = f[i][j] * 2 C(l,2) * (2 j - 1) * C(i,l)
g[i - l][l] = f[i][j] * 2 C(l,2) * (2 j - 1) * C(i,l) * i (当前还没有填的所有点的最短路加1) + g[i][[j]
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,int> pr;
const int inf = 1e9;
const int maxn = 400 + 20;
ll fac[maxn],inv[maxn],f[maxn][maxn],g[maxn][maxn],mod,C2[maxn],pow2[maxn * maxn],pow22[maxn][maxn];
int n;
inline void up(ll &x,ll y){ x = (x + y) % mod; }
inline ll power(ll x,ll y){
ll res = 1;
while ( y ){
if ( y & 1 ) res = res * x % mod;
x = x * x % mod;
y >>= 1;
}
return res;
}
inline ll C(int n,int m){
if ( n < m ) return 0;
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
int main(){ //freopen("input.txt", "r", stdin);
scanf("%d %lld",&n,&mod);
fac[0] = inv[0] = 1;
rep(i,1,n) fac[i] = fac[i - 1] * i % mod , inv[i] = power(fac[i],mod - 2);
rep(i,1,n) C2[i] = C(i,2);
pow2[0] = 1;
rep(i,1,n * n) pow2[i] = pow2[i - 1] * 2 % mod;
rep(i,1,n) rep(j,1,n) pow22[i][j] = power(pow2[i] - 1,j);
f[n - 1][1] = 1 , g[n - 1][1] = 0;
repd(i,n - 1,0){
rep(j,1,n - 1){
if ( !f[i][j] ) continue;
rep(l,1,i){
ll cur = pow2[C2[l]] * pow22[j][l] % mod * C(i,l) % mod;
up(f[i - l][l],f[i][j] * cur);
up(g[i - l][l],g[i][j] * cur + f[i][j] * cur % mod * i);
}
}
}
ll ans = 0;
rep(i,0,n - 1) up(ans,g[0][i]);
ans = ans * power(n - 1,mod - 2) % mod;
cout<<ans<<endl;
}