题意简述
这个有点难简述。
有(n)行(m)列的字母构成的矩阵,每一行和每一列的字符串都由下划线分隔成不同字符串单元。且在同一行由同样的阅读顺序确定的单元均同时不小于或同时不大于它的反串。有些行/列有确定的阅读顺序,则在这些行/列中的按顺序读取的单元称为单词。而部分没有确定的阅读顺序,则有些单元是否为单词是不确定的。你需要找到最少的单元及其本身均为单词的个数,注意,回文串只需计算一次。
建模
有一个小技巧是根据题意和数据范围合理推测所需算法。这里的(n,m)都很小,而且题意满足两者取其一使得总代价最小的形式,考虑使用最小割求解。
首先明确一下目标是求有关于单词的答案,且有关正/反两种状态,则考虑将每个字符串单元的正/反分别设为一个状态。由于在同一行由同样的阅读顺序确定的单元均同时不小于或同时不大于它的反串,则不妨将字典序较小的设为正(若与初始状态相反只需将阅读顺序同时颠倒即可)。
回文串是最容易判断的,直接塞进一个set之后计入答案就好,下面考虑非回文串部分。
先考虑确定阅读顺序的,因为比较简单。怎么才能让割的代价表示的是正反两个状态都是单词的代价呢?设一个源点(s)和汇点(t),若与(s)相连则说明归到集合(S),若与(t)相连则说明归到集合(T),正状态在(S)里才是一个单词,反状态在(T)里才是一个单词。不是回文串的状态不可能同时又正又反的,所以这么规定不会有冲突。如果确定是正着读的,从(s)连向正状态;反之则令反状态连向(t)。这种边是不能割的,将边权设为正无穷。每个正状态向反状态连一条边权为2的边,表示若正反均为单词则必须要割掉这条边来保证割的性质。
而未确定阅读顺序的也是同样的先从正向反连一条边权为2的边。这里的需要我们进行选择的是同一行/列中正的都为单词或反的都为单词。如果本身这一行/列中没有任何一个正/反状态已经被规定是单词了,那么当然是不需要任何代价的,选择任意正反统统连上即可;或它们同时只有正/反是单词,也不需要多余的代价。所以实际上需要考虑的是有些正状态为单词另外有些反状态为单词的情况。
假设有(u,u',v,v')分别表示同一行/列的两个不同正反状态,(u)对应(u'),(v)对应(v'),且(s)连向(u),(v')连向(t)(这里是作为两个只连向一边的代表元素)。这时候就是一个妥协的问题:是(u')连向(t)然后割掉((u, u'))还是(s)连向(v)然后割掉((v,v'))?直观的想法是割少的那一种。这件事情还是留给网络流自己跑的时候考虑吧,我们只需要留给它们一个抗衡的机会。这时候不用真的尝试向(s)或(t)加边,而是从(u')连向(v),构成一个(s->u->u'->v->v'->t)的通路,使得至少要割掉一条边保证割的性质,跑最小割的时候就会自然而然找到少的那一类去割。由于(u)和(v)有很多个,并且我们在连边的时候并不知道最终哪些点是(u)的类型哪些是(v)的类型,所以只需要对于每行/列新建一个虚节点(p),每个单元都从反连到(p),再从(p)连到正即可。这样的边不影响其它的情况,可以自己画图看看。
代码
#include <cstdio>
#include <cctype>
#include <cstring>
#include <string>
#include <map>
#include <set>
#include <iostream>
#include <algorithm>
using std::set;
using std::map;
using std::cin;
using std::string;
using std::swap;
const int INF=0x3f3f3f3f;
const int maxn=10000+10;
const int maxm=100;
string strr[maxm];
int op[2][maxm];
int cur[maxn],head[maxn],to[maxn<<4],nxt[maxn<<4],val[maxn<<4];
int dep[maxn];
int tot,cnt;
int s,t;
set<string> str;
map<string,int> id;
struct Queue
{
int a[maxn];
int l,r;
Queue() {l=1,r=0;}
void push(int x) {a[++r]=x;}
void pop() {l++;}
int front() {return a[l];}
bool empty() {return l>r;}
}q;
int min(int x,int y) {return x<y?x:y;}
void add(int u,int v,int w)
{
nxt[++tot]=head[u];
head[u]=tot;
to[tot]=v;
val[tot]=w;
}
int read()
{
int res=0,p=1;
char ch=getchar();
while(!isdigit(ch))
{
if (ch=='-')
p=-1;
ch=getchar();
}
while(isdigit(ch))
res=res*10+ch-'0',ch=getchar();
return res*p;
}
void work(string a,int flag,int p)
{
string b(a.rbegin(), a.rend());
if (a==b)
{
str.insert(a);
return ;
}
if (a>b)
flag=-flag,swap(a, b);
if (!id[a])
id[a]=++cnt,id[b]=++cnt,add(cnt-1, cnt, 2),add(cnt, cnt-1, 0);
int id_a=id[a],id_b=id[b];
if (flag==1)
add(s, id_a, INF),add(id_a, s, 0);
else if (flag==-1)
add(id_b, t, INF),add(t, id_b, 0);
else
{
add(id_b, p, INF),add(p, id_b, 0);
add(p, id_a, INF),add(id_a, p, 0);
}
}
bool bfs()
{
for (int i=1;i<=cnt;i++)
dep[i]=INF;
dep[s]=0;
q=Queue();
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
for (int i=head[u];i;i=nxt[i])
{
int v=to[i];
if (!val[i]||dep[v]<=dep[u]+1)
continue;
dep[v]=dep[u]+1;
q.push(v);
}
}
return dep[t]!=INF;
}
int dfs(int u,int minf)
{
if (u==t)
return minf;
int used=0;
for (int &i=cur[u];i;i=nxt[i])
{
int v=to[i];
if (!val[i]||dep[v]!=dep[u]+1)
continue;
int flow=dfs(v, min(val[i], minf-used));
if (!flow)
continue;
val[i]-=flow;
val[i^1]+=flow;
used+=flow;
if (used==minf)
break;
}
return used;
}
int main()
{
int T=read();
while(T--)
{
memset(head, 0, sizeof(head));
str.clear(),id.clear();
tot=1,cnt=0;
int n=read(),m=read();
for (int i=1;i<=n;i++)
op[0][i]=read();
for (int i=1;i<=m;i++)
op[1][i]=read();
for (int i=1;i<=n;i++)
{
cin>>strr[i];
strr[i]+='_';
}
for (int i=0;i<=m;i++)
strr[n+1]+='_';
s=++cnt,t=++cnt;
for (int i=1;i<=n;i++)
{
int u=++cnt;
string a="";
for (int j=0;j<=m;j++)
{
if (strr[i][j]!='_')
a+=strr[i][j];
else
{
if (a.size())
work(a, op[0][i], u);
while(j<=m-1&&strr[i][j+1]=='_')
j++;
a="";
}
}
}
for (int i=0;i<m;i++)
{
int u=++cnt;
string a="";
for (int j=1;j<=n+1;j++)
{
if (strr[j][i]!='_')
a+=strr[j][i];
else
{
if (a.size())
work(a, op[1][i+1], u);
while(j<=n&&strr[j+1][i]=='_')
j++;
a="";
}
}
}
int ans=str.size();
while(bfs())
{
for (int i=1;i<=cnt;i++)
cur[i]=head[i];
ans+=dfs(s, INF);
}
printf("%d
",ans);
}
return 0;
}