SPFA(判负环) + 二分(01分数规划) - Word Rings - POJ 2949
题意:
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。
如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连。
我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
如下例:
ababc
bckjaca
caahoynaab
第一个串能与第二个串相连,第二个串能与第三个串相连,第三个串能与第一个串相连,我们按照此顺序相连,便形成了一个环串,长度为 5+7+10=22(重复部分算两次),总共使用了 3 个串,所以平均长度是 223≈7.33。
输入格式
本题有多组数据。
每组数据的第一行,一个整数 n,表示字符串数量;
接下来 n 行,每行一个长度小于等于 1000 的字符串。
读入以 n=0 结束。
输出格式
若不存在环串,输出”No solution.”,否则输出最长的环串的平均长度。
只要答案与标准答案的差不超过 0.01,就视为答案正确。
数据范围
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1≤n≤105
输入样例:
3
intercommunicational
alkylbenzenesulfonate
tetraiodophenolphthalein
0
输出样例:
21.66
分析:
记 环 i 号 单 词 的 长 度 为 l e n i 。 记环i号单词的长度为len_i。 记环i号单词的长度为leni。
目 标 : 环 上 单 词 的 长 度 之 和 与 单 词 个 数 的 比 值 的 最 大 值 , 即 环 上 单 词 的 平 均 长 度 的 最 大 值 m a x ( ∑ l e n i ∑ 1 ) 。 目标:环上单词的长度之和与单词个数的比值的最大值,即环上单词的平均长度的最大值max(\frac{\sum len_i}{\sum 1})。 目标:环上单词的长度之和与单词个数的比值的最大值,即环上单词的平均长度的最大值max(∑1∑leni)。
比 较 直 观 的 01 分 数 规 划 问 题 。 比较直观的01分数规划问题。 比较直观的01分数规划问题。
二 分 答 案 m i d , 转 化 为 判 断 ∑ ( l e n i − m i d × 1 ) > 0 是 否 成 立 。 二分答案mid,转化为判断\sum (len_i-mid×1)>0是否成立。 二分答案mid,转化为判断∑(leni−mid×1)>0是否成立。
也 就 是 判 断 边 权 为 l e n i − m i d 的 图 中 是 否 存 在 正 环 。 也就是判断边权为len_i-mid的图中是否存在正环。 也就是判断边权为leni−mid的图中是否存在正环。
二 分 的 区 间 应 当 是 [ 1 1000 , 1000 ] 。 二分的区间应当是[\frac{1}{1000},1000]。 二分的区间应当是[10001,1000]。
若 最 终 结 果 小 于 1 1000 , 我 们 可 认 为 无 解 。 若最终结果小于\frac{1}{1000},我们可认为无解。 若最终结果小于10001,我们可认为无解。
难点:如何建图?
若 我 们 将 每 个 单 词 看 作 一 个 点 , 上 限 有 1 0 5 个 单 词 , 极 端 情 况 下 每 个 单 词 两 两 间 均 存 在 边 , 若我们将每个单词看作一个点,上限有10^5个单词,极端情况下每个单词两两间均存在边, 若我们将每个单词看作一个点,上限有105个单词,极端情况下每个单词两两间均存在边,
上 限 有 n × ( n − 1 ) ≈ 1 0 10 条 边 , 不 合 理 。 上限有n×(n-1)≈10^{10}条边,不合理。 上限有n×(n−1)≈1010条边,不合理。
本 题 可 将 一 个 单 词 视 作 一 条 边 , 两 个 端 点 分 别 为 首 部 2 个 字 符 和 尾 部 两 个 字 符 , 边 权 为 单 词 的 长 度 。 本题可将一个单词视作一条边,两个端点分别为首部2个字符和尾部两个字符,边权为单词的长度。 本题可将一个单词视作一条边,两个端点分别为首部2个字符和尾部两个字符,边权为单词的长度。
我 们 将 首 位 4 个 字 符 , 分 别 哈 希 为 2 个 整 型 数 作 为 两 个 点 的 编 号 。 我们将首位4个字符,分别哈希为2个整型数作为两个点的编号。 我们将首位4个字符,分别哈希为2个整型数作为两个点的编号。
2 位 字 符 至 多 26 × 26 = 676 种 可 能 , 即 整 个 图 至 多 有 676 个 点 。 2位字符至多26×26=676种可能,即整个图至多有676个点。 2位字符至多26×26=676种可能,即整个图至多有676个点。
于 是 能 够 建 立 不 多 于 676 个 点 , n 条 边 的 图 。 于是能够建立不多于676个点,n条边的图。 于是能够建立不多于676个点,n条边的图。
s p f a 极 端 情 况 的 时 间 复 杂 度 为 O ( n m ) , n × m ≈ 676 × 1 0 5 = 6.76 × 1 0 7 。 spfa极端情况的时间复杂度为O(nm),n×m≈676×10^{5}=6.76×10^7。 spfa极端情况的时间复杂度为O(nm),n×m≈676×105=6.76×107。
说明:
由 于 本 题 的 时 间 限 制 较 严 , 我 们 可 以 针 对 数 据 做 一 些 优 化 。 由于本题的时间限制较严,我们可以针对数据做一些优化。 由于本题的时间限制较严,我们可以针对数据做一些优化。
当 经 过 的 点 的 数 量 超 过 一 个 较 大 值 时 , 我 们 就 可 认 为 是 存 在 环 的 。 当经过的点的数量超过一个较大值时,我们就可认为是存在环的。 当经过的点的数量超过一个较大值时,我们就可认为是存在环的。
比 如 本 题 中 至 多 676 个 点 , 若 更 新 点 的 次 数 超 过 10000 , 我 们 就 认 为 存 在 环 。 提 前 返 回 。 比如本题中至多676个点,若更新点的次数超过10000,我们就认为存在环。提前返回。 比如本题中至多676个点,若更新点的次数超过10000,我们就认为存在环。提前返回。
或 者 用 栈 来 代 替 队 列 , 同 样 能 够 较 快 的 判 断 出 负 环 。 或者用栈来代替队列,同样能够较快的判断出负环。 或者用栈来代替队列,同样能够较快的判断出负环。
queue代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
const int N=676, M=1e5+10;
int m;
int e[M],ne[M],w[M],h[N],idx;
double dis[N];
int cnt[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool check(double mid)
{
memset(st,false,sizeof st);
memset(cnt,0,sizeof cnt);
queue<int> Q;
for(int i=0;i<N;i++)
{
Q.push(i);
st[i]=true;
}
int Count=0;
while(Q.size())
{
int u=Q.front();
Q.pop();
st[u]=false;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(dis[j]<dis[u]+w[i]-mid)
{
dis[j]=dis[u]+w[i]-mid;
cnt[j]=cnt[u]+1;
if(cnt[j]>=N) return true;
if(++Count>10000) return true;
if(!st[j])
{
st[j]=true;
Q.push(j);
}
}
}
}
return false;
}
int main()
{
while(~scanf("%d",&m),m)
{
memset(h,-1,sizeof h);
idx=0;
char str[1010];
int left,right,len;
for(int i=0;i<m;i++)
{
scanf("%s",str);
len=strlen(str);
if(len>=2)
{
left=(str[0]-'a')*26+str[1]-'a';
right=(str[len-2]-'a')*26+str[len-1]-'a';
add(left,right,len);
}
}
double l=0,r=1000;
while(r-l>1e-4)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
if(r<1e-4) puts("No solution.");
else printf("%lf\n",r);
}
return 0;
}
stack代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<stack>
#include<cstdio>
using namespace std;
const int N=676, M=1e5+10;
int m;
int e[M],ne[M],w[M],h[N],idx;
double dis[N];
int cnt[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool check(double mid)
{
memset(st,false,sizeof st);
memset(cnt,0,sizeof cnt);
//stack<int> S;
int stk[N],tt=0;
for(int i=0;i<N;i++)
{
stk[tt++]=i;//S.push(i);
st[i]=true;
}
int Count=0;
while(tt)
{
int u=stk[--tt];//S.top();
//S.pop();
st[u]=false;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(dis[j]<dis[u]+w[i]-mid)
{
dis[j]=dis[u]+w[i]-mid;
cnt[j]=cnt[u]+1;
if(cnt[j]>=N) return true;
//if(++Count>10000) return true;
if(!st[j])
{
st[j]=true;
stk[tt++]=j;//S.push(j);
}
}
}
}
return false;
}
int main()
{
while(~scanf("%d",&m),m)
{
memset(h,-1,sizeof h);
idx=0;
char str[1010];
int left,right,len;
for(int i=0;i<m;i++)
{
scanf("%s",str);
len=strlen(str);
if(len>=2)
{
left=(str[0]-'a')*26+str[1]-'a';
right=(str[len-2]-'a')*26+str[len-1]-'a';
add(left,right,len);
}
}
double l=0,r=1000;
while(r-l>1e-4)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
if(r<1e-4) puts("No solution");
else printf("%lf\n",r);
}
return 0;
}