C.Odd Even Sort
题目描述
有一个长度为 (n) 的排列,要求通过 (n^2) 次操作把他变成 (1,2...n) 的排列:
- 奇数次操作可以操作奇数位置 (x),即交换 (p[x],p[x+1])
- 偶数次操作可以操作偶数位置 (x),即交换 (p[x],p[x+1])
(nleq 500)
解法
(n^2) 次操作这么宽松,可以考虑一下一次把 (1,2,3...) 换到第一个位置,第二个位置,第三个位置 (...)
看上去交换是很容易的,如果目标数字在奇数位置,现在操作也在奇数位置怎么办?这就涉及了浪费步数的问题,递归到 (n=3) 的情况是可以特判的,那么我们考虑 (n>3) 的情况。
可以直接操作目标数字,再操作初始目标数字位置的前一位浪费步数,那么现在奇偶性就对得上了,直接往前一直操作就行。注意如果目标数字在 (n) 这个位置,就操作 (n-2) 浪费步数即可。
对于 (n=3) 一直无脑乱换,可以证明有限步内达到答案状态。
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 505;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,nw,f,ji,ou,p[M];vector<int> ans;
void work(int &nw)
{
if(nw) swap(p[ji],p[ji+1]),ans.push_back(ji);
else swap(p[ou],p[ou+1]),ans.push_back(ou);
nw^=1;
}
void solve(int x)
{
if(x==n-2)
{
while(!(p[n-2]<p[n-1] && p[n-1]<p[n]))
{
if(nw) swap(p[ji],p[ji+1]),ans.push_back(ji);
else swap(p[ou],p[ou+1]),ans.push_back(ou);
nw^=1;
}
return ;
}
for(int i=x;i<=n;i++)
if(p[i]==x)
{
if(i==x) {solve(x+1);return ;}
if(i%2==nw)
{
if(i==n)
{
swap(p[n-2],p[n-1]),ans.push_back(n-2);
nw^=1;
}
else
{
swap(p[i],p[i+1]),ans.push_back(i);
swap(p[i-1],p[i]),ans.push_back(i-1);
i++;
}
}
for(int j=i-1;j>=x;j--)
{
swap(p[j],p[j+1]);
ans.push_back(j);nw^=1;
}
solve(x+1);
return ;
}
}
signed main()
{
T=read();
while(T--)
{
n=read();ans.clear();nw=1;
if(n%2==0) ji=n-1,ou=n-2;
else ji=n-2,ou=n-1;
for(int i=1;i<=n;i++)
p[i]=read();
if(n==2)
{
if(p[1]==1) puts("0
");
else printf("1
1
");
continue;
}
solve(1);
printf("%d
",ans.size());
for(int i=0;i<ans.size();i++)
printf("%d ",ans[i]);
puts("");
}
}
D.1 or 2
题目描述
有 (n) 个物品,每次拿一个或者拿两个,记录下拿走物品的权值和 (x),最小化 (max(x)-min(x))
(1leq nleq 5000)
解法
不难猜到一个贪心就是我们把最大的和最小的配对,但这是可以证明的:
考虑 (Aleq Bleq Cleq D) 两种配对方式满足:
(max(A+C,B+D)geq max(A+D,B+C),min(A+C,B+D)leqmin(A+D,B+C))
显然第二种配对方法最优,所以贪心成立。
然后我来了发贪心交上去 ( t Wa) 穿了,哎,就是我的思维不严谨,我是傻逼。
注意这种贪心的适用范围是只存在两两配对的情况,如果存在单个配对就不行了。但是我们可以加入 (0),让单个配对的东西和 (0) 配对即可,所以我们要枚举加入 (0) 的个数,时间复杂度 (O(n^2))
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int inf = 2e9;
const int M = 10005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M];
signed main()
{
n=m=read();ans=inf;
for(int i=1;i<=n;i++)
a[i]=read();
for(int l=0;l<=n;l++)
{
sort(a+1,a+1+m);
if(m%2==0)
{
int mx=-inf,mi=inf;
for(int i=1;i<=m/2;i++)
{
mx=max(mx,a[i]+a[m-i+1]);
mi=min(mi,a[i]+a[m-i+1]);
}
ans=min(ans,mx-mi);
}
a[++m]=0;
}
printf("%d
",ans);
}
E.Directed Tree
题目描述
给定 (n) 个点的树,求排列 (a) 满足 (a_i) 不是 (i) 的祖先(如果 (i=a_i) 仍然认为合法)
(1leq nleq 2000)
解法
这道题一看就很容斥了,我们枚举 (a_x) 是 (x) 祖先的位置数量 (i),那么有:
其中 (f[i]) 表示钦定 (i) 个位置不合法的方案数,可以用树形 (dp) 算一下,设 (dp[i][j]) 表示以 (i) 为根的子树中钦定了 (j) 个位置不合法的方案数,每次我们考虑让 (u) 的某个儿子 (v) 的 (a_v=u) 而不是让 (a_u) 成为 (u) 的某个祖先,这样只管子树内转移才不会有冲突,然后弄个树上背包合并一下子树不合法位置个数即可。
时间复杂度 (O(n^2))
#include <cstdio>
const int M = 2005;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,tot,ans,f[M],siz[M],tmp[M],fac[M],dp[M][M];
struct edge
{
int v,next;
}e[2*M];
void dfs(int u,int fa)
{
dp[u][0]=1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
dfs(v,u);
for(int i=siz[u];i>=0;i--)
for(int j=siz[v];j>=0;j--)
tmp[i+j]=(tmp[i+j]+dp[u][i]*dp[v][j])%MOD;
siz[u]+=siz[v];
for(int i=0;i<=siz[u];i++)
dp[u][i]=tmp[i],tmp[i]=0;
}
for(int i=siz[u];i>=0;i--)
dp[u][i+1]=(dp[u][i+1]+dp[u][i]*(siz[u]-i))%MOD;
siz[u]++;
}
signed main()
{
n=read();
for(int i=2;i<=n;i++)
{
int j=read();
e[++tot]=edge{j,f[i]},f[i]=tot;
e[++tot]=edge{i,f[j]},f[j]=tot;
}
dfs(1,0);fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=0;i<=n;i++)
{
if(i&1) ans=(ans-dp[1][i]*fac[n-i])%MOD;
else ans=(ans+dp[1][i]*fac[n-i])%MOD;
}
printf("%lld
",(ans+MOD)%MOD);
}
F.Logical Operations on Tree
题目描述
给定一棵 (n) 个节点的树,每个节点会被标记 (0/1),每条边会被标记为 AND/OR
进行任意 (n-1) 次操作,每次操作选择有边相连的两个点合并成一个新的点,新点的标记为原来点标记经过边对应位运算的结果。
求最后整棵树能只剩下标记 (1) 为的点的树的数量。
(nleq 10^5)
解法
思维方式真的很重要,这道题是树上删除问题,从叶子角度考虑最简单,我们讨论叶子和对应边的情况:
- 叶子标记为 (1),边为
or
,那么叶子以外的部分任意操作,最后都能合法。 - 叶子标记为 (1),边为
and
,那么这个点一定无影响。 - 叶子标记为 (0),边为
or
,那么这个点一定无影响。 - 叶子标记为 (0),边为
and
,那么操作这个点会让父亲为 (0),优先操作一定最优。
综上,除了第一种情况,我们其它都可以无脑操作叶子。明确上面的结论之后我们可以暴力讨论 (dp),但是官方给出更方便的做法。设 (f[i]) 表示子树内不存在 1 or
的情况,(g[i]) 表示 (f[i]) 的这些情况中合法情况个数,转移:
- 考虑边是
and/or
,除掉1 or
的情况:(f[u]leftarrow(2f[v]-g[i])cdot f[u]) - 考虑是
1 and
或者0 or
:(g[u]leftarrow g[u]cdot f[u])
最后的答案是 (2^{2n-1}-f[1]+g[1]),这是一个简单的容斥。
#include <cstdio>
const int M = 100005;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,tot,f[M],F[M],G[M];
struct edge
{
int v,next;
}e[2*M];
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
void dfs(int u,int fa)
{
F[u]=2;G[u]=1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
dfs(v,u);
F[u]=(F[v]*2-G[v])*F[u]%MOD;
G[u]=G[u]*F[v]%MOD;
}
}
signed main()
{
n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
e[++tot]=edge{v,f[u]},f[u]=tot;
e[++tot]=edge{u,f[v]},f[v]=tot;
}
dfs(1,0);
printf("%lld
",(qkpow(2,2*n-1)-F[1]+G[1]+MOD)%MOD);
}