Canada Tour
题目大意
双向连通图,点从左向右排列,
你需要先从最左的点到最右的点,(过程中只能从左向右走)
然后再从最右的点返回最左的点,(过程中只能从右向左走)
过程中除了最左的点,其它点都至多能经过一次
求最多能经过的点的个数
题解
从右向左走反过来,就是说从左向右走,题目变成从最左两条不相交到达最右的路径,经过最多的点
一个问题是如何解决没有重复的点
这里的解决方案是
dp[i][j]
表示没有重复的点的情况下 一条路径走到点i,一条路径走到点j,经过的点的最大的个数
在状态转移的时候需要保证新的状态有i<j
,
dp[i][j] = dp[i][k]+1
,如果k->j
有路径, 我们保证了除了初始点dp[0][0]=1
以外,任何i不等于0,有dp[i][i] = 0
,
证明一下
首先任何可达的状态不会遗漏,假设存在路径 一边到i,一边到j,(不妨设i<j
)那么有它的来源一定能从[i][k]
来
再不重复点证明
抛开初始点
因为保证了i<j
,dp[i][j]
的来源仅为dp[i][k]
,我们有k一定不等于i,所以只要dp[i][k]
是没有重复点的即可
因此递归可证明,这样的dp是不会经过重复点,
最后考虑都到达最右的点,那么发现和dp[i][最右]
的 经过的点数一致,注意的是 注意判断点i到最右点是否有路径
#include <bits/stdc++.h>
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
using namespace std;
const string filename = "tour";
void usefile(){
freopen((filename+".in").c_str(),"r",stdin);
freopen((filename+".out").c_str(),"w",stdout);
}
char s[210],t[210];
map<string,int>str2idx;
int n,m,mp[110][110],dp[110][110];
int main(){
usefile();
scanf("%d%d",&n,&m);
rep(i,0,n){
scanf("%s",s);
str2idx[s]=i;
}
rep(i,0,m){
scanf("%s %s",s,t);
mp[str2idx[s]][str2idx[t]]=1;
mp[str2idx[t]][str2idx[s]]=1;
}
int ans=1;
dp[0][0]=1;
rep(i,0,n){
rep(j,i+1,n){
rep(k,0,j){
if(mp[j][k]&&dp[i][k]&&dp[i][k]+1>dp[i][j]){
dp[i][j]=dp[j][i]=dp[i][k]+1;
}
}
}
if(mp[i][n-1]){
ans=max(ans,dp[i][n-1]);
}
}
printf("%d\n",ans);
}
Character Recognition
题目大意
先提供空格和26个小写字母的 字符画01矩阵,每个字符都是20*20
然后 你需要解析一段n*20
字符矩阵,n行20列
这段矩阵和标准的差异是,
- 对于一个字符,可能某一行被倍增了 变成21行,它紧接着倍增那行
- 对于一个字符,可能某一行被吞了 变成19行
- 0 和 1 和真实值不同
上面问题可以存在的组合有,和原始完全一致,单纯1,单纯2,1+3,2+3,其中 0和1 的改变率小于等于30%
题目呢,可以说相当于 USACO帮我们建了个OCR的模型!!!我们在该模型下实现算法
题解
f[i]表示从最开始到第i行最小误差
f[i] = min(f[i-19]+19行来匹配,f[i-20]+20行来匹配,f[i-21]+21行来匹配)
我们预先处理 所有字符的行(27*20
) 和 目标匹配的行N
O(N*27*20)
然后 直接dp,O(N*(20*3))
理论上如果做了前缀和后缀和优化
实现如下
#include <bits/stdc++.h>
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
using namespace std;
const string filename = "charrec";
void usefile(){
freopen((filename+".in").c_str(),"r",stdin);
freopen((filename+".out").c_str(),"w",stdout);
}
int n,m;
char str[]=" abcdefghijklmnopqrstuvwxyz";
char s[30][30][30];// 标准字符集[idx][i][j]
char t[1210][30]; // 目标字符串[i][j]
int diff[30][30][1210]; // 预处理 [字符idx][字符的i行][目标的j行] = 01差异和
tuple<int,int,int>dp[1210]; // {最小代价,父节点,字符}
const int SUP = 1000000;
// 从st行开始匹配len行,返回{最小的代价,匹配的字符};
pair<int,int> solve(int st,int len){
pair<int,int>ret= {SUP,-1};
rep(i,0,27){
if(len==20){
int sum=0;
rep(k,0,20){
sum+=diff[i][k][st+k];
}
ret= min(ret,{sum,i});
}else{
// 这边重复计算了, 这里可以用前缀和 后缀和继续优化, 目测可以优化掉约10-20倍性能
// 不过因为USACO的数据比较小 这样已经是0.1s内了 就没写优化了
rep(j,0,20){ // 枚举删掉或增加的行
int p=st,sum=0;
rep(k,0,j){
sum+=diff[i][k][p++];
}
if(len==21){ // 19为删掉 21为增加
sum+=diff[i][j][p++];
sum+=diff[i][j][p++];
}
rep(k,j+1,20){
sum+=diff[i][k][p++];
}
ret= min(ret,{sum,i});
}
}
}
return ret;
}
int main() {
ios::sync_with_stdio(false);
freopen("font.in","r",stdin);
freopen("charrec.out","w",stdout);
scanf("%d",&n);
rep(idx,0,27){
rep(i,0,20){
scanf("%s",s[idx][i]);
}
}
fclose(stdin);
freopen("charrec.in","r",stdin);
scanf("%d",&m);
rep(i,0,m){
scanf("%s",t[i]);
dp[i] = {SUP,0,0};
}
// 预处理 把每个字符的每一行 都和 目标字符比
// 目标k行 和 第x个字符 的y行 比较不同的01个数
rep(idx,0,27){
rep(i,0,20){
rep(mm,0,m){
rep(j,0,20){
diff[idx][i][mm]+=s[idx][i][j]!=t[mm][j];
}
}
}
}
rep(i,18,m){
rep(len,19,22){
auto [cost,idx]=solve(i-len+1,len);
dp[i] = min(dp[i], {cost+(i-len<0?0:get<0>(dp[i-len])),i-len,idx});
}
}
vector<char>ans;
int i=m-1;
do{
ans.push_back(str[get<2>(dp[i])]);
}while((i=get<1>(dp[i]))>0);
per(itr,0,ans.size()){
printf("%c",ans[itr]);
}
printf("\n");
return 0;
}
Telecowmunication
题目大意
100点,无向图
网络流,最小字典序的最小割点
记得前不久才有一个USACO的 最大流问题
题解
老生常谈了,=。=难道是我练题的顺序不对,感觉在刚刚学完最大流 最小割的时候,就会学到拆点啊。
然后直接最小割点就出来了,然后字典序就依次枚举 再计算?想了想编码似乎不可行 1 + 100
vs 2+3
若都是可行的,显然前面的字典序小
#include <bits/stdc++.h>
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)
using namespace std;
const string filename = "telecow";
void usefile(){
freopen((filename+".in").c_str(),"r",stdin);
freopen((filename+".out").c_str(),"w",stdout);
}
int n,m,c1,c2;
int p2p[210][210];
int vis[210];
int flow[210][210];
void clearvis(){
rep(i,1,2*n+1){
vis[i]=false;
}
}
void dup(){
rep(i,1,2*n+1){
rep(j,1,2*n+1){
flow[i][j]=p2p[i][j];
}
}
}
int stk[210];
int bfs(int idx,int dst){
clearvis();
int st = 0,rear=0;
stk[rear++]=idx;
vis[idx] = true;
while(st<rear){
int p = stk[st];
rep(i,1,n*2+1){
if(vis[i])continue;
if(flow[p][i]){
if(i == dst){
return true;
}
stk[rear++]=i;
vis[i]=true;
}
}
st++;
}
return false;
}
int dfs(int idx,int dst){
if(idx == dst){
return 1;
}
vis[idx] = true;
rep(i,1,2*n+1){
if(vis[i])continue;
if(!flow[idx][i])continue;
int r = dfs(i,dst);
if(r){
flow[idx][i] -= r;
flow[i][idx] += r;
return r;
}
}
return 0;
}
int maxflow(){
int ret =0;
while(bfs(c1*2,c2*2-1)){
clearvis();
ret+=dfs(c1*2,c2*2-1);
}
return ret;
}
void addp(int p1,int p2){
int p1i=p1*2-1;
int p1o=p1*2;
int p2i=p2*2-1;
int p2o=p2*2;
if(p1!=c1 && p2 != c2){
p2p[p2o][p1i] = 1;
}
if(p1!=c2 && p2 != c1){
p2p[p1o][p2i] = 1;
}
}
int main(){
usefile();
cin>>n>>m>>c1>>c2;
rep(i,0,m){
int a,b;
scanf("%d %d",&a,&b);
addp(a,b);
}
rep(i,1,n+1){
p2p[i*2-1][i*2]=1;
}
dup();
int ans = maxflow();
cout<<ans<<endl;
vector<int>ps;
rep(i,1,n+1){
if(i== c1 || i==c2){
continue;
}
dup();
flow[i*2-1][i*2]=0;
int ret = maxflow();
if(ret == ans-1){
ps.push_back(i);
ans-=1;
p2p[i*2-1][i*2]=0;
}
}
rep(i,0,ps.size()){
printf("%d%c",ps[i]," \n"[i==ps.size()-1]);
}
return 0;
}
总结
第一题的DP的方法,我要是打cf没遇到,估计是想不出怎么处理路径不重复点 的 这样的状态转移
第二题的DP实现没啥好说的,但这样一个OCR模型 感觉也是很“实际”
第三题 emmmm 感觉刚学完网络流的时候 就知道拆点,好像没什么特别的。