区间dp,树形dp,状压dp 总结
A.Sue 的小球
Description
在平面直角坐标系上给你 \(n\) 个下坠的小球的下坠速度和初始高度,你的初始位置在 \((x_0,0)\),当你移动到某一个小球的正上或正下时可以瞬间将其收集,收集到的价值为当前小球纵坐标的千分之一,请求出在收集所有小球的情况下,收集到的价值最大值。
Solution
因为可以在瞬间收集小球,所以当经过小球 \(i\) 时,直接收集显然更优,所以最终收集的一定时一个区间,考虑区间 DP。
设 \(dp_{i,j,0/1}\) 表示收集完 \(i\) 到 \(j\) 这个区间后停留在左边或右边的小球下降的最小值,最后只需要用小球初始高度之和减去 \(\min(dp_{i,j,0},dp_{i,j,1})\) 再统一乘上千分之一就是答案。
因为我们要将所有的小球都收集到,所以我们可以统计一下所有小球的下落速度前缀和,转移的时候只需要加上所有未被收集的小球的下坠速度之和乘转移的时间即可。
Code
#include<bits/stdc++.h>
#define N 1005
using namespace std;
int n,xs,f1[N][N],f2[N][N],ans,s[N];
struct node{
int x,y,v;
}a[N];
int cmp(node a,node b){return a.x<b.x;}
signed main(){
cin>>n>>xs;
for(int i=1;i<=n;i++)cin>>a[i].x;
for(int i=1;i<=n;i++)cin>>a[i].y;
for(int i=1;i<=n;i++)cin>>a[i].v;
a[++n].x=xs;
sort(a+1,a+1+n,cmp);
memset(f1,0x3f,sizeof f1);
memset(f2,0x3f,sizeof f2);
for(int i=1;i<=n;i++){
ans+=a[i].y;
if(a[i].x==xs&&a[i].y==0&&a[i].v==0)f1[i][i]=f2[i][i]=0;
s[i]=s[i-1]+a[i].v;
}
for(int len=2;len<=n;len++){
for(int i=1;i<=n-len+1;i++){
int j=i+len-1;
f1[i][j]=min(f1[i][j],f1[i+1][j]+(a[i+1].x-a[i].x)*(s[n]-s[j]+s[i]));
f1[i][j]=min(f1[i][j],f2[i+1][j]+(a[j].x-a[i].x)*(s[n]-s[j]+s[i]));
f2[i][j]=min(f2[i][j],f1[i][j-1]+(a[j].x-a[i].x)*(s[n]-s[j-1]+s[i-1]));
f2[i][j]=min(f2[i][j],f2[i][j-1]+(a[j].x-a[j-1].x)*(s[n]-s[j-1]+s[i-1]));
}
}
printf("%.3lf",(ans-min(f1[1][n],f2[1][n]))/1000.0);
return 0;
}
B.Replace on Segment
Description
给你一个数组,你每次可以选择一个区间,并将这个区间内的所有数字变成一个不属于这个区间内的数,求将整个区间变为同一个数的最小步数。
Solution
很明显的区间 DP。用两个数组 \(f\) 和 \(g\),\(f_{i,j,k}\) 表示将 \(i\) 到 \(j\) 这个区间的数都转成 \(k\) 的最小步数,\(g_{i,j,k}\) 表示将 \(i\) 到 \(j\) 这个区间的数都转成不是 \(k\) 的最小步数。
转移方程为:
Code
#include<bits/stdc++.h>
#define N 105
using namespace std;
int T,n,x,a[N],f[N][N][N],g[N][N][N],ans;
signed main(){
cin>>T;
while(T--){
cin>>n>>x;
memset(f,0x3f,sizeof f);
memset(g,0x3f,sizeof g);
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=1;j<=x;j++){
f[i][i][j]=(j!=a[i]);
g[i][i][j]=(j==a[i]);
}
}
for(int len=1;len<=n;len++){
for(int i=1;i<=n-len+1;i++){
int j=i+len-1,mn=0x3f3f3f3f;
for(int k=1;k<=x;k++){
for(int m=i;m<j;m++)g[i][j][k]=min(g[i][j][k],g[i][m][k]+g[m+1][j][k]);
mn=min(mn,g[i][j][k]);
}
for(int k=1;k<=x;k++)g[i][j][k]=min(g[i][j][k],mn+1);
for(int k=1;k<=x;k++){
for(int m=i;m<j;m++)f[i][j][k]=min(f[i][j][k],f[i][m][k]+f[m+1][j][k]);
f[i][j][k]=min(f[i][j][k],g[i][j][k]+1);
}
}
}
ans=0x3f3f3f3f;
for(int i=1;i<=x;i++)ans=min(ans,f[1][n][i]);
cout<<ans<<"\n";
}
return 0;
}
C.Candles
Description
给你 \(n\) 个蜡烛,蜡烛 \(i\) 在 \(x_i\) 上,长度为 \(a_i\),以每秒减一的速度燃烧,烧完熄灭。初始时你在原点,每次移动一个单位长度,在蜡烛 \(i\) 时可以将蜡烛 \(i\) 熄灭,并且不耗费时间,熄灭后的蜡烛不再减短。求最终留下的蜡烛总长度最长是多少。
Solution
因为熄灭蜡烛不需要时间,所以只要经过蜡烛 \(i\),就可以直接将其熄灭,所以熄灭的蜡烛一定是一个区间,我们考虑区间 DP。
设 \(f_{i,j,t,0/1}\) 表示已经熄灭了 \(i\) 到 \(j\) 的区间,花费了 \(t\) 的时间,最后停在在 \(i\) 或 \(j\) 的最大值。转移方程为:
但我们发现 \(t\) 这一维会很大,时间空间都会炸,所以把它换一下,变成 \(f_{i,j,k,0/1}\) 表示已经熄灭了 \(i\) 到 \(j\) 的区间,还剩 \(k\) 个正在燃烧,最后停在在 \(i\) 或 \(j\) 的最大值。
我们给 \(n\) 加一在原点处加一个没有长度的蜡烛,设它的编号为 \(s\),则 \(f_{s,s,k,0/1}=0(0\le k< n)\),其余都初始化为极小值。
那么 \(k\) 这一维是如何转移的呢?分为两种情况:
- 当前要熄灭的蜡烛已经灭了,也就是直接由 \(k\) 转移。
- 当前要熄灭的蜡烛还在燃烧,而就是从 \(k+1\) 转移。
转移方程如下:
最终答案为 \(\max(f_{1,n,0,0},f_{1,n,0,1})\)。
Code
#include<bits/stdc++.h>
#define int long long
#define N 305
using namespace std;
int n,f1[N][N][N],f2[N][N][N],ans;
struct node{int x,l;}a[N];
int cmp(node a,node b){return a.x<b.x;}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].l;
if(abs(a[i].x)>=a[i].l)a[i].l=0;
}
++n;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=0;k<=n;k++)f1[i][j][k]=f2[i][j][k]=-0x3f3f3f3f3f3f3f3f;
for(int i=1;i<=n;i++){
if(!a[i].x&&!a[i].l){
for(int k=0;k<n;k++)f1[i][i][k]=f2[i][i][k]=0;
break;
}
}
for(int len=2;len<=n;len++){
for(int i=1;i<=n-len+1;i++){
int j=i+len-1;
for(int k=0;k<=n-len;k++){
f1[i][j][k]=max(f1[i][j][k],f1[i+1][j][k]-k*(a[i+1].x-a[i].x));//i 已经熄灭
f1[i][j][k]=max(f1[i][j][k],f2[i+1][j][k]-k*(a[j].x-a[i].x));
f1[i][j][k]=max(f1[i][j][k],a[i].l+f1[i+1][j][k+1]-(k+1)*(a[i+1].x-a[i].x));//i 还未熄灭
f1[i][j][k]=max(f1[i][j][k],a[i].l+f2[i+1][j][k+1]-(k+1)*(a[j].x-a[i].x));
//
f2[i][j][k]=max(f2[i][j][k],f2[i][j-1][k]-k*(a[j].x-a[j-1].x));//j 已经熄灭
f2[i][j][k]=max(f2[i][j][k],f1[i][j-1][k]-k*(a[j].x-a[i].x));
f2[i][j][k]=max(f2[i][j][k],a[j].l+f2[i][j-1][k+1]-(k+1)*(a[j].x-a[j-1].x));//j 还未熄灭
f2[i][j][k]=max(f2[i][j][k],a[j].l+f1[i][j-1][k+1]-(k+1)*(a[j].x-a[i].x));
}
}
}
cout<<max(f1[1][n][0],f2[1][n][0]);
return 0;
}
D.Vertex Deletion
Description
给定一棵树,其中有 \(N\) 个节点。求出满足以下条件的点 \(u\) 的数量:
- 把 \(u\) 和连接 \(u\) 的边全部删除后得到的图的最大匹配与原树的最大匹配相等。
Solution
我们可以先跑一遍 DP,求出整棵树的最大匹配,然后通过换根的方式来判断每一个 \(u\) 是否相等。
设 \(dp_{i,0}\) 表示 \(i\) 节点不与子节点相连可以构成的在以 \(i\) 为根节点的子树中的最大匹配,转移方程如下:
然后换根转移即可。
Code
#include<bits/stdc++.h>
#define N 200005
#define int long long
using namespace std;
int n,vis[N],dp[N][2],cnt,ans,sum[N],num[N],snm[N],nid[N],son[N];
vector<int>g[N];
void dfs(int x){
num[x]=snm[x]=-0x3f3f3f3f;
if(g[x].size()>1||x==1)dp[x][1]=1;
if(g[x].size()==1&&x!=1)num[x]=0;
for(int y:g[x]){
if(vis[y])continue;
vis[y]=1;
++son[x];
dfs(y);
dp[x][0]+=dp[y][1];
dp[x][1]+=dp[y][1];
if(dp[y][0]-dp[y][1]>num[x]){
snm[x]=num[x],nid[x]=y;
num[x]=dp[y][0]-dp[y][1];
}
else if(dp[y][0]-dp[y][1]>snm[x])snm[x]=dp[y][0]-dp[y][1];
}
dp[x][1]+=num[x];
}
void dfs1(int x){
for(int y:g[x]){
if(vis[y])continue;
vis[y]=1;
sum[x]+=max(dp[y][0],dp[y][1]);
int a=dp[x][0],b=dp[x][1];
--son[x],++son[y];
dp[x][0]-=max(dp[y][0],dp[y][1]);
dp[x][1]-=dp[y][1]+(!son[x]);
if(nid[x]==y){
dp[x][1]-=num[x];
if(son[x])dp[x][1]+=snm[x];
}
sum[y]+=max(dp[x][0],dp[x][1]);
dp[y][0]+=max(dp[x][0],dp[x][1]);
dp[y][1]+=dp[x][1]+(son[y]==1);
int nw=num[y];
if(dp[x][0]-dp[x][1]>num[y]){
snm[y]=num[y],nid[y]=x;
num[y]=dp[x][0]-dp[x][1];
}
else if(dp[x][0]-dp[x][1]>snm[y])snm[y]=dp[x][0]-dp[x][1];
dp[y][1]+=num[y]-nw;
dfs1(y);
++son[x],--son[y];
dp[x][0]=a,dp[x][1]=b;
}
if(sum[x]==cnt)++ans;
}
signed main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
vis[1]=1;
dfs(1);
cnt=max(dp[1][0],dp[1][1]);
memset(vis,0,sizeof vis);
vis[1]=1;
dfs1(1);
cout<<ans;
return 0;
}
E.Perils in Parallel
Description
有 \(n\) 个电灯,编号从 \(1\) 到 \(n\) ,第 \(i\) 个电灯在 \(a_i\) 处,状态为 \(b_i (b_i=0/1)\)。有 \(m\) 个开关,编号从 \(1\) 到 \(m\) ,第 \(i\) 个开关控制 \(l_i\) 到 \(r_i\),如果按下开关则所有 \(l_i\) 到 \(r_i\) 中的电灯状态取反。求是否有一组可行解,使得所有电灯状态都为 0。没有输出-1,有则输出方案。请注意:方案需要排序后输出。
Solution
因为题目要求的是改变电灯状态,所以可以很容易想到用异或来表示。我们可以通过异或差分数组来保存。最后要求的是全部为零,即差分数组中的所有元素都应为 \(0\)。
我们想到每次修改 \(l\) 到 \(r\) 的区间,只需要给 \(d_l\) 和 \(d_{r+1}\) 异或 1 即可,所以我们先通过二分确定每一次操作所修改的区间,然后将节点 \(l\) 和节点 \(r+1\) 建一条边,然后在建成的这棵树上跑 DP:如果节点 \(x\) 为 1,那么就将 \(x\) 和 \(x\) 的父亲节点都异或 1,同时记录修改的路径的编号,如果最后根节点为 1,就说明不行,直接输出 -1,否则就将前面记录的路径全部输出。
但是最后建成的可能不是以棵棵的树,所以我们需要通过生成树来保证 DP 可以跑下去。
Code
#include<bits/stdc++.h>
#define N 200005
using namespace std;
int n,m,d[N],vis[N],f[N];
vector<pair<int,int> >g[N];
vector<int>ans;
struct node{
int a,b;
}t[N];
int cmp(node a,node b){
return a.a<b.a;
}
int findf(int x){
if(x==f[x])return x;
return f[x]=findf(f[x]);
}
int findl(int x){
int l=1,r=n,res=1;
while(l<=r){
int mid=(l+r)>>1;
if(t[mid].a<x)l=mid+1;
else r=mid-1,res=mid;
}
return res;
}
int findr(int x){
int l=1,r=n,res=n;
while(l<=r){
int mid=(l+r)>>1;
if(t[mid].a>x)r=mid-1;
else l=mid+1,res=mid;
}
return res;
}
void dfs(int x){
for(auto p:g[x]){
int y=p.first;
if(vis[y])continue;
vis[y]=1;
dfs(y);
if(d[y])ans.push_back(p.second),d[y]=0,d[x]^=1;
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>t[i].a>>t[i].b;
sort(t+1,t+1+n,cmp);
for(int i=1;i<=n+1;i++)d[i]=(t[i].b^t[i-1].b),f[i]=i;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
l=findl(l),r=findr(r);
if(l>r)continue;
int ln=findf(l),rn=findf(r+1);
if(ln==rn)continue;
f[ln]=findf(rn);
g[l].push_back({r+1,i});
g[r+1].push_back({l,i});
}
for(int i=1;i<=n+1;i++){
if(!vis[i]){
vis[i]=1;
dfs(i);
if(d[i]&&i<=n){
cout<<"-1";
return 0;
}
}
}
sort(ans.begin(),ans.end());
cout<<ans.size()<<"\n";
for(int out:ans)cout<<out<<" ";
return 0;
}
F.Optimal Path Decomposition
Description
给定一个 \(n\) 个点的树,你可以将树划分为若干条不交的路径,每条路径上的点,染一种颜色。找到最小的 \(K\) 满足:对于任意一条原树上的路径,其经过的点的颜色数不超过 \(K\)。
Solution
个人认为这是最难的一道。首先这题一眼看上去就是二分,这样我们就将求最小值问题表示成了求可行性问题。因为只能是同一条路径上的颜色相同,所以某个节点最多只能与两个子节点颜色相同。二分 \(K\) 的值,并用 \(K\) 来限定转移,最后如果根节点可以被转移到,那么当前的 \(K\) 就是可行的。设 \(dp_{i,0}\) 表示 \(i\) 节点不与子节点颜色相同,在以 \(i\) 为根节点的子树中,最小的最大路径经过的颜色数量,\(dp_{i,1}\) 和 \(dp_{i,2}\) 分别表示与一个或两个子节点颜色相同的值。
转移方程很麻烦,具体看代码。
Code
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define int long long
#define N 200005
using namespace std;
int n,dp[N][3],vis[N],ans;
vector<int>g[N];
void dfs(int x,int k){
dp[x][0]=dp[x][1]=dp[x][2]=0;
for(int y:g[x]){
if(vis[y])continue;
int a0=inf,a1=inf,a2=inf;
vis[y]=1;
dfs(y,k);
if(dp[x][0]+dp[y][1]<k)a1=min(a1,max(dp[x][0],dp[y][1]));
if(dp[x][1]+dp[y][1]<k)a2=min(a2,max(dp[x][1],dp[y][1]));
if(dp[x][0]+dp[y][2]+1<k)a0=min(a0,max(dp[x][0],dp[y][2]+1));
if(dp[x][1]+dp[y][2]+1<k)a1=min(a1,max(dp[x][1],dp[y][2]+1));
if(dp[x][2]+dp[y][2]+1<k)a2=min(a2,max(dp[x][2],dp[y][2]+1));
dp[x][0]=a0,dp[x][1]=a1,dp[x][2]=a2;
dp[x][1]=min(dp[x][1],dp[x][0]),dp[x][2]=min(dp[x][2],dp[x][1]);
}
}
int check(int mid){
memset(vis,0,sizeof vis);
vis[1]=1;
dfs(1,mid);
if(dp[1][2]<=mid)return 1;
return 0;
}
signed main(){
cin>>n;
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
g[a].push_back(b);
g[b].push_back(a);
}
int l=1,r=n;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid))r=mid-1,ans=mid;
else l=mid+1;
}
cout<<ans;
return 0;
}
G.Cut and Reorder
Description
给你长为 \(n(n\le 22)\) 的 \(A\),\(B\) 两个数组和一个整数 \(c\),可以对 \(A\) 数组做出以下两种操作:
- 将 \(A_i\) 加上 \(x\),代价为 \(\left |x \right |\)。
- 将 \(A\) 数组划分为任意 \(x\) 个子段,并将其随意排列,代价为 \(c\times (x-1)\)。
求将 \(A\) 数组转化成 \(B\) 数组的最小代价。
Solution
看到 \(n\)最大为 22,立马就想到了 DP。我们可以将两种操作看成是将 \(A\) 里的数字按任意顺序挑选然后往 \(B\) 里面从前往后填。设 \(dp_{s,i}\) 表示当前在 \(A\) 数组中选择的状态为 \(s\)(也就是当前 \(B\) 数组填了 \(s\) 中为 1 的数量个),填在 \(B\) 中的最后一个是 \(A_i\) 的最小代价。转移方程即为:
其中 \(num(s)\) 表示状态 \(s\) 中为 1 的数量。但这样复杂度会炸,所以我们换一种想法:与其一个个地往里填,倒不如一段段地往里填,每次填的时候直接加 \(c\) 即可。设 \(dp_{s}\) 表示状态为 \(s\) 的最小代价。转移具体看代码。
Code
#include<bits/stdc++.h>
#define N 22
#define lowbit(x) (x&(-x))
#define ll long long
using namespace std;
int n;
ll a[N+1],b[N+1],c,dp[1<<N];
signed main(){
cin>>n>>c;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
memset(dp,0x3f,sizeof dp);
dp[0]=-c;
for(int s=0;s<(1<<n);s++){
int cnt=0;
for(int t=s;t;t-=lowbit(t))++cnt;
for(int i=1;i<=n;i++){
if(s&(1<<(i-1)))continue;
int t=s;
ll num=c;
for(int j=i;!(s&(1<<(j-1)))&&j<=n;j++){
t|=(1<<(j-1));
num+=abs(a[j]-b[cnt+j-i+1]);
dp[t]=min(dp[t],dp[s]+num);
}
}
}
cout<<dp[(1<<n)-1];
return 0;
}
E.E or m
Description
开始你有一个 \(n\times m(n,m\le 18)\) 全 0 矩阵,你可以随意执行如下操作:
- 选择任意一行,将其从最左端开始的连续一段染成 1。
- 选择任意一列,将其从最上端开始的连续一段染成 1。
如果一个矩阵可以由此得到,那么这个矩阵被称为好的。
现在你有一个 01? 矩阵 \(a\),你需要将所有 ? 替换为 0 或 1,问得到的矩阵中有多少个是好的。答案对 998244353 取模。
Solution
我们看到 \(n\) 和 \(m\) 小于等于 18,所以还是想到状压 DP。设 \(dp_{i,j,s,k=0/1}\) 表示当前 \(i,j\) 的位置,每一列是否都全为 1 的状态,这一排到目前是否全为 1 的状态下,一共有多少种方案。正常转移即可,转移时枚举当前点是 0 或 1,同时对应更新当前行和列的状态,在每一行的最后一个注意换行。
Code
#include<bits/stdc++.h>
#define N 18
#define mod 998244353
using namespace std;
int n,m,dp[N+1][N+1][1<<N][2],ans;
char c[N+1][N+1];
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)c[0][i]='1';
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>c[i][j];
}
}
for(int i=2;i<=n;i++){
for(int j=2;j<=m;j++){
if(c[i][j]=='1'&&c[i-1][j]=='0'&&c[i][j-1]=='0'){
cout<<0;
return 0;
}
}
}
if(c[1][1]!='0')dp[1][1][(1<<m)-1][1]=1;
if(c[1][1]!='1')dp[1][1][(1<<m)-1][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(i==n&&j==m)break;
for(int s=0;s<(1<<m);s++){
for(int k=0;k<2;k++){
int x=i,y=j+1;
if(y>m)x=i+1,y=1;
if(c[x][y]!='1')(dp[x][y][s&(~(1<<(y-1)))][0]+=dp[i][j][s][k])%=mod;
if(c[x][y]!='0'&&(k||(s&(1<<(y-1)))||y==1)){
if(y>1)(dp[x][y][s][k]+=dp[i][j][s][k])%=mod;
else (dp[x][y][s][1]+=dp[i][j][s][k])%=mod;
}
}
}
}
}
for(int s=0;s<(1<<m);s++){
(ans+=(dp[n][m][s][0]+dp[n][m][s][1])%mod)%=mod;
}
cout<<ans;
return 0;
}
I. Vowels
Description
给出n个长度为3的由′a′~′x′组成的单词,一个单词是正确的当且仅当其包含至少一个元音字母。 这里的元音字母是a-x的一个子集。 对于所有元音字母集合,求这n个单词中正确单词的数量平方的异或和。
Solution
看题解之前觉得自己是个不会做,看了题解第一句话后觉得还觉得自己是个连这题都不会做。
题解的第一句话是:正难则反。
什么意思呢?因为只有 24 个字母,所以肯定是状压。我们原来关注的是选择 \(s\) 的状态下有多少个可行的,但是只要一个单词中有一个选择的字母就可以,导致转移起来十分困难(我想到了容斥,奈何太菜不会写),那我们转换过来,只要有一个可以就满足,不就等同于所有不行就不满足吗?设 \(dp_s\) 表示不选择的字母的状态下(即选择为 0,不选择为 1),有多少个不满足的。最后统计的时候,只需要用 \(n\) 减去 \(dp_s\) 即可。
转移的时候需要用到这样一个东西:高维前缀和。我们发现如果直接用 \(dp_{s\otimes (1<<i)}\to dp_s(s\&(1<<i))\) 来转移的话,还是会出现容斥的问题,那么我们可以用高位前缀和来解决。他的基础代码长这样:
for(int i=0;i<n;i++){
for(int s=0;s<(1<<n);s++){
if(s&(1<<i))dp[s]+=dp[s^(1<<i)];
}
}
我们发现他是先枚举位数,再枚举状态,为什么是这样呢? 我们发现这个时候转移的 \(t\) 其实是不完全的,而通过这种不完全的 \(t\) 的,刚好可以和别的错开。举个例子:0 到 111 的转移。转移顺序如下,
010 -> 011
100 -> 101
110 -> 111
001 -> 011
100 -> 110
101 -> 111
001 -> 101
010 -> 110
011 -> 111
我们关注 111,最后转移到他身上的就相当于是完整的 001 和 110,不存在容斥的问题。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,dp[1<<24],ans;
string str;
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>str;
int s=((1<<(str[0]-'a'))|(1<<(str[1]-'a'))|(1<<(str[2]-'a')));
dp[s]++;
}
for(int i=0;i<24;i++){
for(int s=0;s<(1<<24);s++){
if(s&(1<<i))(dp[s]+=dp[s^(1<<i)]);
}
}
for(int s=0;s<(1<<24);s++)ans^=(n-dp[s])*(n-dp[s]);
cout<<ans;
return 0;
}

浙公网安备 33010602011771号