了解set 4个集合间的操作
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
void test(){
set x1,x2;
//do something ...
set_union(ALL(x1), ALL(x2), INS(x));//并集操作
set_intersection(ALL(x1), ALL(x2), INS(x));//交集操作
set_difference(ALL(x1), ALL(x2), INS(x))//取差集
set_symmetric_difference(ALL(x1), ALL(x2), INS(x))//取对称差集
}
UVA-1593
代码对齐水题
水得晕厥,但是还被坑了,最后一列不可以再补上空格。这种格式题就是瞻前顾后,边界最容易出错。而且题意要看清不能马马虎虎过,对输出格式完全理解才开始写。
#include
using namespace std;
string str[1005][1000];
int cnt[2000];
int mx_col;
int mx_len[2000];
int main(){
string s;
int num=0;
while(getline(cin,s)){
stringstream ss(s);
while(ss>>str[num][cnt[num]]){
cnt[num]++;mx_col=max(mx_col,cnt[num]);
}
num++;
}
for(int i=0;i<mx_col;i++){
for(int j=0;j<num;j++){
mx_len[i]=max(mx_len[i],(int)str[j][i].length());
}
}
for(int i=0;i<num;i++){
for(int j=0;j<cnt[i];j++){
if(j!=0)cout<<' ';
cout<<str[i][j];
if(j==cnt[i]-1){
cout<<endl;break;
}
for(int k=0;k<mx_len[j]-str[i][j].length();k++)cout<<' ';
}
}
return 0;
}
一开始想直接dp,dp[i][j][k]表示到达第i行第j列染色为k的格子最少花费的金币,但当k=0时,不知道它上次是保持了何种颜色,从没有颜色的格子到有颜色的格子这个状态比较难转移,还要记录上一次的选择颜色感觉太麻烦,需要回溯的样子,所以还是选择dfs写。忘记一个小小的剪枝和回溯位置放错又tle又wa,然后换bfs。写完ac之后顺便把dfs也检查出来了。
其实想用dp是因为看错题了以为只能从左上角向右下角移动,导致写dfs也是一样写法…所以完全理解题意再行动!(何况这是中文题呀!)
#include
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int c[N][N],ans,m,n;
int dx[4]={0,0,-1,1},dy[4]={1,-1,0,0};
int res[N][N];
void dfs(int x,int y,int cur,int add){
if(cur>=res[x][y])return;//一开始忘记判断是否在当前情况还需要继续回溯 一直tle
res[x][y]=cur;
if(x==m&&y==m)ans=min(ans,cur);
//dfs用于更新下一个状态 所以当前必定有颜色 但是可以回溯
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||ny<1||nx>m||ny>m)continue;
if(c[nx][ny]){
if(c[x][y]==c[nx][ny])dfs(nx,ny,cur,0);
else dfs(nx,ny,cur+1,0);
}
else if(!add){
c[nx][ny]=c[x][y];
dfs(nx,ny,cur+2,c[x][y]);
c[nx][ny]=0;//一开始忘记在这里回溯了 只有30分
}
}
if(add)c[x][y]=0;
}
int main(){
cin>>m>>n;
ans=inf;
memset(res,0x3f,sizeof res);
for(int i=1;i<=n;i++){
int x,y,z;cin>>x>>y>>z;
c[x][y]=z+1;
}
dfs(1,1,0,0);
if(ans>=inf)cout<<-1<<endl;
else cout<<ans<<endl;
return 0;
}
就是个bfs的水题。情况有点多,把一样的操作归并在一起,把不同的一点点挑出来,可能代码量比较少,看起来也比较清晰。
#include
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int c[N][N],ans,m,n,dis[N][N];
int dx[4]={-1,1,0,0},dy[4]={0,0,1,-1};
int add[N];
struct Node{
int x,y,dis,c;
bool operator<(const Node a)const{return a.dis<dis;}
};
priority_queue<Node>q;
void bfs(){
q.push({1,1,0,c[1][1]});
while(!q.empty()){
Node cur=q.top();q.pop();
int x=cur.x,y=cur.y;
if(x==m&&y==m){
cout<<cur.dis<<endl;
return ;
}
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||ny<1||nx>m||ny>m)continue;
if(c[x][y]){
int add=0;//把一样的操作归并在一起,把不同的一点点挑出来
if(!c[nx][ny])add=2;
else if(c[nx][ny]!=c[x][y])add=1;
if(dis[nx][ny]>dis[x][y]+add){
dis[nx][ny]=dis[x][y]+add;
if(add==2)q.push({nx,ny,dis[nx][ny],c[x][y]});
else q.push({nx,ny,dis[nx][ny],c[nx][ny]});
}
}
else {
int add=0;
if(!c[nx][ny])continue;
if(cur.c!=c[nx][ny])add=1;
if(dis[nx][ny]>dis[x][y]+add){
dis[nx][ny]=dis[x][y]+add;
q.push({nx,ny,dis[nx][ny],c[nx][ny]});
}
}
}
}
cout<<-1<<endl;
}
可能这个题要用A*,哈希什么的,但是我就用一个dijkstra就过了,然后加了亿点点细节,其实主要就是用map实现游戏网格和数字的转换。有兴趣可以看一下,思路很简单,(但是要开O2优化才能过,我也不是很懂。)
#include
#include
#include
#include
#include
#include
using namespace std;
#define inf 0x3f3f3f3f
typedef pair<int,int> PII;
const int N=2E6+10;//只有362880种状态,每种状态最多可以向4个方向转移,4*362880=1,451,520,N为2E6则可。
//Part 1:------------------------------------------------------------------------链式前向星存图,降低时间&空间复杂度
int n,cnt,head[N],vis[N],dis[N];
struct E{
int to,nxt,w;
}e[N];
void add(int u,int v,int w){
e[++cnt]={v,head[u],w};
head[u]=cnt;
}
//Part 2:------------------------------------------------------------------------处理序号和图之间的关系
map<int ,int>reflect;//reflect[i]=j:i保存整个图,有0位数,j相当于i的序号。i从小到大按顺序存储。
int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0};//dx表示行的移动,dy表示列的移动
int power10[10];//保存10的幂次,避免重复计算
int match[N];//match[i]=j:第i个位置保存的是j图
void pre(){
//获得0-8所有的排列
int pos=0;
int a[10]={0,1,2,3,4,5,6,7,8};
do{
int sum=0;
for(int i=0;i<=8;i++){
sum=sum*10+a[i];
}match[++pos]=sum,reflect[sum]=pos;
}while(next_permutation(a,a+9));
//获得10的1-9的幂次结果
power10[0]=1;
for(int i=1;i<=9;i++)power10[i]=power10[i-1]*10;
}
//用序号寻找0的位置
PII ReflectToZeroPosition(int i){
PII GridPosition;//GridPosition保存序号对应的图中0所在的位置(行,列)
int tmp=match[i];
int ArrayPosition=0;
while(tmp%10!=0){
tmp/=10;
ArrayPosition++;
}
ArrayPosition=8-ArrayPosition;
GridPosition={ArrayPosition/3,ArrayPosition%3};
return GridPosition;
}
int CheckTransform(int CurState,int direction){//如果当前的状态可以向某个方向转移就直接返回那个方向的状态,否则返回-1
PII CurPosition=ReflectToZeroPosition(CurState);
int next_x=CurPosition.first+dx[direction],next_y=CurPosition.second+dy[direction];
if(next_x>-1&&next_x<3&&next_y>-1&&next_y<3){
int NextDigit=next_x*3+next_y,CurDigit=CurPosition.first*3+CurPosition.second;
int tmp=match[CurState],num=tmp/power10[8-NextDigit]%10;
tmp=tmp-num*power10[8-NextDigit]+num*power10[8-CurDigit];
return reflect[tmp];
}
return -1;
}
void OutputGrid(int i){
int grid=match[i];
for(int i=8;i>-1;i--){
printf("%d ",grid/power10[i]%10);
if(i==6||i==3||i==0)
printf("\n");
}
}
//Part 3:---------------------------------------------------------------------------------- dijkstra计算单源最短路
struct Node{
int dis,pos;
bool operator <(const Node &x)const{
return x.dis<dis;
}
};
priority_queue<Node>q;
int s,ed,fa[N];//s为源点,ed为终点,fa用于记录路径,从终点沿着fa回溯到起始点。
void dijkstra(){
q.push({0,s});
dis[s] = 0;
while(!q.empty()){
Node cur=q.top();//每次都取出队列中到达源点距离最短的点(优先队列会自动对队列排序)
if(cur.pos==ed)return;
q.pop();
if(vis[cur.pos])continue;
vis[cur.pos]=1;
for(int i=head[cur.pos];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]>dis[cur.pos]+e[i].w&&!vis[v]){
fa[v]=cur.pos;
dis[v]=dis[cur.pos]+e[i].w;
q.push(Node{dis[v],v});
}
}
}
}
stack<int>path;
int main(){
pre();
n=362880;
ed=123804765;
scanf("%d",&s);
ed=reflect[123804765];
s=reflect[s];
for(int i=1;i<=n;i++)dis[i]=inf;
for(int i=1;i<N;i++)e[i].w=1;
for(int i=1;i<=n;i++){
for(int j=0;j<4;j++){
int v=CheckTransform(i,j);
if(v!=-1){add(i,v,1);}
}
}
dijkstra();
int i=reflect[123804765];
while(i!=0){
path.push(i);
i=fa[i];
}
int step=0;
printf("%d\n",(int)path.size() - 1);
return 0;
}
其实剪枝我现在真的是摸不着头脑,把写过的奇技淫巧优秀剪枝方法都稍微记录一下。
dfs的框架其实很简单,每次dfs有一个当前状态,通过某些状态转移方法继续dfs,进入下一状态,若当前状态是目标状态就可以更新答案。
1.记忆化搜索:某个状态是否搜到就确定,是否一个状态会被一直重复。
2.约束满足:先搜分支少的,减少搜索层数。
3.若要求最优解,则可以通过比较当前状态的解和已经获得的最优解决定当前是否可以直接返回。
一股dfs的浓浓的味道。将当前状态设为第 c u r cur cur只猫,即对第 c u r cur cur只猫进行分配,决定它待在哪个车里,目标状态设为 c u r = n + 1 cur=n+1 cur=n+1,即 n n n只猫都有车可搭了。状态转移方法则是将当前的猫 c u r cur cur放在不同编号的车里。
优化:
#include
using namespace std;
#define ll long long
const ll inf=1e18;
ll a[20];
ll w,n,ans;
bool vis[20];
bool cmp(ll x,ll y){return x>y;}
void dfs(ll cnt,ll last){
if(cnt>=ans)return;//如果已经大于当前答案不必再搜
int fl=1;
for(int i=1;i<=n;i++){
if(!vis[i]){
fl=0;break;
}
}
if(fl){
if(last!=w)cnt++;
ans=min(ans,cnt);
return;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
vis[i]=1;
if(last<a[i]){
dfs(cnt+1,w-a[i]);
}
else if(last==a[i]){
dfs(cnt+1,w);
}
else dfs(cnt,last-a[i]);
vis[i]=0;
}
}
}
int main(){
cin>>n>>w;
ans=inf;
for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+1+n,cmp);
dfs(0,w);
cout<<ans<<endl;
return 0;
}
记忆化搜索就是一个非常棒的剪枝,在搜索到某个位置如果这个位置已经有答案了,换句话说到这里你已经知道再往下搜会是什么结果了(因为你之前已经搜过了),那么直接使用这个答案就好,不用再继续搜下去。使用场景大概是(根据我写的并不多的题),题目里的状态有明显的单调性,比如高度和次数,就是我们可以确定已经搜索到的答案具有唯一性,一旦搜到就不会再改变了,因为记忆化搜索就是dp和搜索的结合,要保证无后效性。
先祭出一道最经典的题目,用简单的dfs会tle,原因是什么呢?有很多个地方会重复搜索。
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
用样例来说明一下,我们从第一个位置搜索到最后一个位置。
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
ans=max(ans,dfs(i,j));
}
}
当我们搜到 25 25 25那个位置,因为它是最高的点,会向右侧的 20 20 20继续dfs,换句话说,我们要知道 20 20 20的最长轨道才能判断是否用 20 20 20的轨道去更新 25 25 25的轨道。更新完 25 25 25就该更新 20 20 20了,这时又要累死累活地重新dfs去计算 20 20 20的最长轨道,但如果我们在更新 25 25 25的时候保存了 20 20 20的最长轨道,这时候就可以直接使用了, 20 20 20直接不用计算,轨道越长,我们省下来的时间越多。
#include
using namespace std;
const int N=3e2+10;
int g[N][N],dp[N][N];
int n,m,ans;
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
int dfs(int x,int y){
if(dp[x][y]!=-1)return dp[x][y];
int f1=0,f2=0;
for(int i=0;i<4;i++){
int nx=dx[i]+x,ny=dy[i]+y;
if(nx<1||nx>n||ny<1||ny>m)continue;
f1++;
if(g[nx][ny]>=g[x][y]){
f2++;
}
else dp[x][y]=max(dp[x][y],dfs(nx,ny)+1);
}
if(f1==f2)return dp[x][y]=1;
return dp[x][y];
}
int main(){
cin>>n>>m;
memset(dp,-1,sizeof dp);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>g[i][j];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
ans=max(ans,dfs(i,j));
}
}
cout<<ans<<endl;
return 0;
}
首先想到暴力bfs,但某个状态会被重复放到队列中很多次,记忆化搜索的优化点是将这些重复的状态记录下来,状态dp[i][j][k]表示走到第i行第j列且当前花费k时的状态数。若dp[i][j][k]不为0,则说明这个状态已经在队列中出现过,且当前肯定还没有被弹出队列。为什么呢?bfs的过程是这样的:
因此随着每次层数加深,k值不减,即记忆化的必然是同一层的,若第k时的状态数不为0,则它只可能在当前层出现过,因为还没有到达下一层,当前层的所有状态都还没有被弹出队列,所以只需要改变其状态数而没必要将该状态再次放入队列,即队列中此时必然存在该状态。
以后都不会代码补全了!s给我补全成x…
#include //参考题解
using namespace std;
const int N=1e2+10;
char g[N][N],dp[N][N][20];
int n,m,t,sx,sy,ex,ey;
int dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
struct Node{int x,y,s;};
void bfs(){
queue<Node>q;
Node node={sx,sy,0};
q.push(node);
dp[sx][sy][0]=1;
while(!q.empty()){
Node cur=q.front();q.pop();
int x=cur.x,y=cur.y,s=cur.s;
//cout<
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i],ns=s+1;
if(dp[nx][ny][ns]){
dp[nx][ny][ns]+=dp[x][y][s];
continue;
}
if(nx<1||ny<1||nx>n||ny>m||g[nx][ny]=='*'||ns>t)continue;
dp[nx][ny][ns]=dp[x][y][s];
q.push({nx,ny,ns});
}
}
cout<<dp[ex][ey][t]<<endl;
}
void solve(){
cin>>n>>m>>t;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>g[i][j];
cin>>sx>>sy>>ex>>ey;
bfs();
}
int main(){
int T=1;
while(T--){
solve();
}
}
采用记忆化搜索的方式记录第1行可达的左右区间,这样做需要对第 1 1 1行每个位置都进行1次dfs,但其实因为第 1 1 1行也有高度差,可以互达,有很多次的 d f s dfs dfs是重复的,妥妥超时。
void dfs(int fy,int x,int y){//对于第一行每个点fy都需要dfs一次才能获得其左右端点。
vis[x][y]=fy;
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||ny<1||nx>n||ny>m||g[nx][ny]>=g[x][y]||vis[nx][ny]==fy)continue;
if(nx==n){
dp[fy][0]=min(dp[fy][0],ny);
dp[fy][1]=max(dp[fy][1],ny);
}
dfs(fy,nx,ny);
}
}
改成记录每个点可达的左右端点,这样就不必每次都 d f s dfs dfs每个点了。
void dfs(int x,int y){
vis[x][y]=1;
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||ny<1||nx>n||ny>m||g[nx][ny]>=g[x][y])continue;
if(!vis[nx][ny])dfs(nx,ny);
l[x][y]=min(l[x][y],l[nx][ny]);
r[x][y]=max(r[x][y],r[nx][ny]);
}
}
最后是区间覆盖
struct Node{
int l,r;
bool operator<(const Node a)const{
if(a.r==r)return l<a.l;
return r<a.r;
}
}node[N];
sort(node+1,node+1+cnt);
int num=0,cur=0;
for(int i=1;i<=cnt;i++){
if(i<cnt&&node[i+1].l<=cur+1)continue;//此处忘记+1...现在彻底会了
num++;
cur=node[i].r;
if(cur==m)break;
}
对记忆化搜索的理解深了一点,一开始那种应该是假的记忆化,因为其实每次都还是从上到下,并没有办法记录什么有用的信息。后面那种是从下往上,可以将已经计算好的结果一层层往上传。区间问题忘记+1…闭区间闭区间闭区间!
粗浅的理解:对dfs的一种改进,假定最优解的步数已经知道,通过估值函数判断当前状态是否可以在限制的步数内向最优解转移。因为朴素的dfs具有很大的盲目性,IDA*通过估值函数赋予dfs一种"智能的性质“,让它在不可能达到解的时候停下来,进行剪枝优化。同时估值函数应该比真实值小,直观的理解是,太大的估值函数可以会”跨过“真实答案,而小的估值函数虽然需要更长的搜索时间,但可以获得答案。
int evaluate(){
int cnt=0;
for(int i=1;i<=n;i++){
if(abs(a[i]-a[i+1])!=1)cnt++;
}
return cnt;
}
合理设计估价函数,既要保证它是有左右的,如以上函数保存相邻的错位值,我们知道每次翻转只能改变一对相邻数的差值,而我们的估价函数就是相邻数的最小差值,(无论差值是多大我们都只设置为1),从而保证我们的估价函数小于真实值。设计的时候要知道我们的步骤可以优化“什么”(比如此题的“什么”就是相邻数差值),通过”什么“来设计估值函数,其实是用预设的最优解的作用来设计估价函数。
虽然知道会错但还是尝试思考的思路
这道题和上一道差不多,只是有最多的步数限制,我们先枚举枚举从0到15的步数,然后设计估价函数,最后利用估价函数去协助dfs过程剪枝。
估价函数设计:我们从答案出发,即步数可以对什么产生影响,每移动一个骑士,我们至多可以改变一个错位的骑士数,也可能无法改变错位骑士数量,比如当空格在黑色阵营内,同时我们移动的是黑色骑士。因此我们可以设计估价函数为当前的白色与黑色错位骑士数量,这样的估价函数可以确保小于真实步数。
char g[10][10];
const char const_g[6][6]={"11111","01111","00*11","00001","00000"};
int evaluate(){
int cnt;
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(g[i][j]!=const_g[i][j])cnt++;
}
}
return cnt;
}
根据题意,只有空格附近八个位置才能移动。
最小步数从小到大枚举,若当前步数可以达到指定的目标状态则结束。在dfs中传入4个参数,分别是当前空格的坐标,当前已经使用的步数,和当前估价函数的值。在dfs过程中枚举空格旁边8个位置,再更新一下估值。更新之后取下一个位置继续dfs,同时增加当前步数,dfs返回之后回溯。
以下是全T代码…
#include
using namespace std;
char g[10][10];
const char ans_g[6][6]={"11111","01111","00*11","00001","00000"};
int dx[8]={-2,-2,2,2,1,1,-1,-1},dy[8]={1,-1,1,-1,2,-2,2,-2};
int ans;
bool fl;
int evaluate(){
int cnt=0;
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(g[i][j]!='*'&&g[i][j]!=ans_g[i][j])cnt++;
}
}
return cnt;
}
void dfs(int x,int y,int eval,int step){
if(fl)return;
if(step==ans){
if(!eval){
cout<<ans<<endl;
fl=1;
}
return;
}
for(int i=0;i<8;i++){
int tmp=eval;
int nx=x+dx[i],ny=y+dy[i];
if(nx<0||ny<0||nx>4||ny>4)continue;
if(ans_g[x][y]==g[nx][ny])tmp--;
if(ans_g[nx][ny]==g[nx][ny])tmp++;
swap(g[nx][ny],g[x][y]);
dfs(nx,ny,tmp,step+1);
swap(g[nx][ny],g[x][y]);
}
}
void solve(){
fl=0;
for(int i=0;i<5;i++)for(int j=0;j<5;j++)cin>>g[i][j];
int x=-1,y=-1;
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(g[i][j]=='*'){
x=i;y=j;break;
}
}if(x!=-1)break;
}
for(ans=0;ans<=15;ans++){
dfs(x,y,evaluate(),0);
if(fl)return ;
}
if(!fl)cout<<-1<<endl;
return ;
}
int main(){
int T;cin>>T;
while(T--)
solve();
return 0;
}
下一步就是最关键的一步!看题解…
(知道一定会错还是努力地去尝试,也许这就是骑士精神吧!)
最大问题:我根本就没有用到我的估价函数…只要在每次进行下一次的dfs加上下面这句话就可以过!
if(evaluate()+step<=ans)//上述简直是失去灵魂的IDA*!!