难题集锦
No 1.飞扬的小鸟
大体意思是说,现在给你一张地图,从最左边出发,最右边停止,地图上有些管子(占上边或下边一条线),每列可以选择点击屏幕上升或者不点下降一定的高度,可以点击多次上升高度累加,触到天花板再顶还在天花板,碰到地板或者管子就算输,问最少点击几下,如果必须输,最多越过几个管子
注意到可以点多次的问题,类似于完全背包,但有一个重要不同,每个位置可以由点多次的状态转移,也可以由点一次的状态转移,所以状态转移的时候需要多加一步,一定要注意天花板要单独处理,一定要注意管子的高度问题,把细节问题做对了,其实还是很水的
//这是后来改的 #include<iostream> #include<cstdio> #include<string> #include<cstring> #include<algorithm> using namespace std; const int maxn = 10005,maxnum = 100000; struct vain{ int high; int low; }; int n,m,k,up[maxn],down[maxn],dp[maxn][1005],vis[maxn]; vain upp[maxn]; int main(){ cin>>n>>m>>k; for(int i = 0;i < n;i++){ cin>>up[i]>>down[i]; } int p,l,h; for(int i = 1;i <= n+1;i++) { for(int j = 0;j <= m;j++){ dp[i][j] = maxnum; } upp[i-1].high = m+1; upp[i-1].low = 0; } dp[0][0] = maxnum; int arrive = k; for(int i = 1;i <= k;i++){ cin>>p>>l>>h; upp[p].high = h; upp[p].low = l; vis[p] = 1; } for(int i = 1;i <= n;i++){ for(int j = 1;j <= m;j++){ if(j >= up[i-1]){ dp[i][j] = min(dp[i][j],min(dp[i-1][j-up[i-1]]+1,dp[i][j-up[i-1]]+1)); } if(j == m){ for(int q = m-up[i-1] ;q <= m;q++) dp[i][j] = min(dp[i][j],min(dp[i-1][q] + 1,dp[i][q] + 1)); } } for(int j = upp[i].low+1;j < upp[i].high;j++) if(j + down[i-1] <=m) dp[i][j] = min(dp[i][j], dp[i-1][j+down[i-1]]); for(int j = 1;j <= upp[i].low;j++) dp[i][j] = maxnum; for(int j = upp[i].high;j <= m;j++) dp[i][j] = maxnum; } int cnt = k, ans = maxnum; for (int i = n; i >= 1; i--) { for (int j = upp[i].low+1; j <= upp[i].high-1; ++j) if (dp[i][j] < maxnum) ans = min(ans, dp[i][j]); if (ans != maxnum) break; if (upp[i].high <= m) cnt --; } if(cnt==k) printf("1\n%d\n", ans); else printf("0\n%d\n", cnt); return 0; } //这是70分的,留着锻炼眼力 #include<iostream> #include<cstdio> #include<string> #include<cstring> #include<algorithm> using namespace std; const int maxn = 10005,maxnum = 100000; struct vain{ int high; int low; }; int n,m,k,up[maxn],down[maxn],dp[maxn][maxn],vis[maxn]; vain upp[maxn]; int main(){ cin>>n>>m>>k; for(int i = 0;i < n;i++){ cin>>up[i]>>down[i]; } int p,l,h; for(int i = 1;i <= n+1;i++) { for(int j = 0;j <= m;j++){ dp[i][j] = maxnum; } upp[i-1].high = maxn; upp[i-1].low = -maxn; } dp[0][0] = maxnum; for(int i = 1;i <= k;i++){ cin>>p>>l>>h; upp[p].high = h; upp[p].low = l; vis[p] = 1; } for(int i = 1;i <= n;i++){ for(int j = 1;j <= m;j++){ if(j!=m){ if(j <= upp[i].low) continue; if(j >= upp[i].high) break; if(j - up[i-1] > 0) dp[i][j] = min(dp[i][j],min(dp[i-1][j-up[i-1]]+1,dp[i][j-up[i-1]]+1)); if(j + down[i-1] <= m) dp[i][j] = min(dp[i][j],dp[i-1][j+down[i-1]]); } if(j == m){ for(int q = m-up[i-1] ;q <= m;q++) dp[i][j] = min(dp[i][j],min(dp[i-1][q] + 1,dp[i][q] + 1)); } } } int ans = maxnum,key = 1,sea = n-1,sign,acc = 0; for(int i = 1;i <= m;i++) ans = min(ans,dp[n][i]); if(ans == maxnum) key = 0; if(key) cout<<1<<endl<<ans; else{ for(;sea >= 0;sea--){ for(int i = 1;i <= m;i++){ if(dp[sea][i] < maxnum) sign = 1; } if(sign) break; } for(int i = 0;i < sea;i++) if(vis[i]) acc++; cout<<0<<endl<<acc; } return 0; }
No.2 搭建双塔
意思是说有不超过100块高度不等的水晶加起来高度不过2000,搭建两座塔,求最大高度
两座塔高度相等,很容易先到会把所有两座塔的高度的情况暴力都求一边,然后用一个vector记录状态,用数组判重,如果有高度相同的情况记录下来,最后输出就行了
#include<iostream> #include<cstdio> #include<algorithm> #include<vector> using namespace std; int n,h[105],ans = 0; short vis[2005][2005]; vector<int> p[2005];//p[i]记录A塔高i的情况下,B塔能取到的高度 int main(){ cin>>n; for(int i = 1;i <= n;i++) scanf("%d",&h[i]); p[0].push_back(0); vis[0][0] = -1; int ma = 0,mb = 0; for(int i = 1;i <= n;i++){ int ucan = ma; for(int j = 0;j <= ucan;j++){ int uban = p[j].size(); for(int k = 0;k < uban;k++){ if(j == p[j][k] && j > ans) ans = j; if(vis[j][p[j][k]] == i) continue; if(!vis[j][h[i]+p[j][k]]){ vis[j][h[i]+p[j][k]] = i; p[j].push_back(h[i]+p[j][k]); } if(!vis[j + h[i]][p[j][k]]){ vis[j + h[i]][p[j][k]] = i; p[j + h[i]].push_back(p[j][k]); ma = max(ma,j+h[i]); } } } } if(ans == 0)cout<<"Impossible"; else cout<<ans<<endl; return 0; }
这个暴力代码,用tyvj测评,虽然最后几个点300ms+,但还是能过的,但在本地用cena测试,最后几个点的时间高达4000ms+,说明这个暴力算法的效率是相当低的,原因是我们只是在判定可能的情况,而忽视了动态规划最优子结构这一基本特性,导致每次放水晶都要把之前所有的情况全部遍历一遍,时间复杂度接近o(n3)
我们分析一下,动规写的不好主要是因为状态转移的不好,状态转移的不好主要是因为状态设计的不好,以前开两个高度的做法显然不利于当前的答案从最优的子状态中推出,从最优的子状态中推出必须要满足在某一状态参量的条件下,以前取到的最小的值都绝对不会用来推出正确的答案,通过分析建塔的过程我们发现,放水晶的目的总是把高塔变高,低塔变高,或者低塔变高塔,改变的看似是某一个塔的高度,实际上改变的是高度差,而高度差一定的情况下,两塔高度总和低的总是不如总和高的更优,所以我们改变最高塔的高度(这么设计就不必考虑两塔水晶选取不重复的问题了,这是解决问题的关键)为要求的最大值,设计高度差为状态,然后每考虑一块水晶放置时考虑以上三种情况和不放此水晶为子状态进行递推,最后高度差为零的状态就是最优解
细节问题上,用了背包里的两行处理,优化空间复杂度
#include <cstdio> #include <cstring> #include <algorithm> #include <iostream> using namespace std; const int v = 2000 + 5; const int MaxN = 100 + 5; int N, sum, num[MaxN], dp[2][v]; int main() { freopen("gold.in","r",stdin); freopen("gold.out","w",stdout); int i, j, k; cin >> N; for(i = 0; i < N; ++i) { cin >> num[i]; sum += num[i]; } memset(dp, -1, sizeof(dp)); dp[1][0] = 0; int a; for(i = 0; i < N; ++i) { a = i % 2; memset(dp[a], -1, sizeof(dp[a])); for(j = 0; j <= sum; ++j) { if(dp[a ^ 1][j] > -1) dp[a][j] = dp[a ^ 1][j]; if(num[i] > j && dp[a ^ 1][num[i] - j] > -1) dp[a][j] = max(dp[a][j], dp[a ^ 1][num[i] - j] + j); if(j + num[i] <= sum && dp[a ^ 1][j + num[i]] > -1) dp[a][j] = max(dp[a][j], dp[a ^ 1][j + num[i]]); if(j >= num[i] && dp[a ^ 1][j - num[i]] > -1) dp[a][j] = max(dp[a][j], dp[a ^ 1][j - num[i]] + num[i]); } } if(dp[a][0] > 0) cout << dp[a][0] << endl; else cout << "Impossible" << endl; }
No.3传纸条
从左上角选两条路线使权值之和最大,要求路线没有重复的点
如果只是一条路线那很好解决,但是如果现在是两条路线,主要矛盾就集中在不能重复,因为选了一条路线,下一条路线可能根本无法到达,也就是说,当前的路线选择,不仅要使这次最大,更要为下次最大铺好道路,这样,如果只设计两维状态,则当前决策不仅依赖于现在的状态,更要依赖以前(以后)的状态,既然有这种依赖关系,那么必须先加上两维来满足无后效性
再考虑重复路线的问题,由于动规的最终目标是两个路线值最优,不优的肯定不取,再结合如果右下角有值就只加一次这种情况,我们在转移的时候也使重复的格子权值只累加一次,这样,当状态转移的时候,就会因为交汇的点少取了一次值而果断放弃掉
再考虑优化,主要在状态上做文章,发现无论怎么走,走过的格子总数是不变的,即每次行走都会使曼哈顿距离减一,所以可以有x1 + y1 = x2 + y2,成立的前提是x,y都是正的,这样不但少了一轮循环,也少去了一个走的远一个走的近的情况,由此就能过了
#include<iostream> #include<cstdio> #include<cstring> #include<string> #include<algorithm> #define maxn 60 using namespace std; int m,n,a[maxn][maxn],dp[maxn][maxn][maxn][maxn]; int main(){ cin>>m>>n; for(int i = 1;i <= m;i++){ for(int j = 1;j <= n;j++){ cin>>a[i][j]; } } for(int i = 1;i <= m;i++){ for(int j = 1;j <= n;j++){ for(int k = 1;k <= m;k++){ int l = i + j - k; if(l <= 0) continue; dp[i][j][k][l] = max(max(dp[i-1][j][k-1][l],dp[i][j-1][k-1][l]),max(dp[i-1][j][k][l-1],dp[i][j-1][k][l-1])) + a[i][j] + ((i == k && j == l) ? 0 : a[k][l]); } } } cout<<dp[m][n][m][n]; return 0; }
No.4 长途旅行
一张有重边和自环的无向图,一个人从0点出发,走到n-1处停止,一个城市可以反复走多次,每走一条边花费一定时间,是否能正好花费时间T
一看到能否,就知道是一种判定问题,主要矛盾在于,如果这么跑的话,会出现一个城市反复跑很多次,才能累加到T的情况,而累加到T,T<=10^18,时间上不允许,所以要压缩,而压缩最好的办法就是取余,在图论里减少走的次数的最好办法就是最短路,要把两者结合起来,发现从0点出来的一条边来回走可以形成一个2w的自环,于是用2w进行压缩,记录到每个点的距离,在mod2w的情况下的最小值,这样比这个值大的都可以看成是走过几次环之后的结果,这样就可以舍掉不要了,最后看dist[n-1][n%2w],如果结果不大于T,都可以用上几个2w填补,如果超了或者干脆到不了,就不行
既然是取余,肯定要选择最小的出边,这样状态也更少,时间上也优
#include<iostream> #include<cstdio> #include<string> #include<cstring> #include<algorithm> #include<queue> #include<vector> #include<stack> #include<map> #define inf 987654321 #define ll long long using namespace std; char ch; ll t,n,m,md; struct edge{ int v; int w; }; struct dat{ int d; int u; }; vector<edge> g[55]; bool vis[55][10005]; ll dist[55][10005]; inline int read(){ int x = 0,f = 1; char ch = getchar(); while(ch<'0' || ch>'9'){ if(ch=='-')f=-1; ch = getchar(); } while(ch>='0'&&ch<='9'){ x = x*10+ch-48; ch = getchar(); } return x*f; } inline ll Read(){ ll x = 0,f = 1; char ch = getchar(); while(ch<'0' || ch>'9'){ if(ch=='-')f=-1; ch = getchar(); } while(ch>='0'&&ch<='9'){ x = x*10+ch-48; ch = getchar(); } return x*f; } void spfa(){ memset(dist,63,sizeof(dist)); memset(vis,false,sizeof(vis)); dist[0][0] = 0; dat tmp; tmp.u = tmp.d = 0; queue<dat> q; q.push(tmp); while(!q.empty()){ tmp = q.front(); q.pop(); int u = tmp.u,d = tmp.d,dd,to; vis[u][d] = false; for(int j = 0;j < g[u].size();j++){ to = g[u][j].v; dd = g[u][j].w + d; int nmod = dd%md; if(dist[to][nmod] > dd){ dist[to][nmod] = dd; if(!vis[to][nmod]){ vis[to][nmod] = true; tmp.d = nmod; tmp.u = to; q.push(tmp); } } } } } int main(){ freopen("travel.in","r",stdin); freopen("travel.out","w",stdout); int T = read(); edge tmp; while(T--){ n = read(); m = read(); t = Read(); md = inf; for(int i = 1;i <= m;i++){ int u=read(),v=read(),w=read(); tmp.v = v; tmp.w = w; g[u].push_back(tmp); tmp.v = u; g[v].push_back(tmp); if(md > w && (!u||!v)) md = w; } md*=2; spfa(); if(dist[n-1][t%md] <= t) cout<<"Possible"<<endl; else cout<<"Impossible"<<endl; for(int i = 0;i <= n;i++) g[i].clear(); } return 0; }
No.5 数论题
求a1x1+a2x2+a3x3+……+anxn = dy中的最小正整数解(是让y的值尽可能的小的正整数)
用图论方法最短路来做,从零点开始,记录d的剩余系中每个点已求得的多项式的最小值,最后0的情况整除d就是最优解(注意a=0的情况)
#include<iostream> #include<cstdio> #include<string> #include<cstring> #include<algorithm> #include<map> #include<vector> #include<queue> #define ll long long using namespace std; ll n,d,dist[40005]; int vis[40005],flag = 0; ll a[40005]; void spfa(){ flag++; memset(dist,0,sizeof(dist)); queue<int> q; q.push(0); ll now,nxt,step; while(!q.empty()){ now = q.front(); q.pop(); vis[now] = 0; for(int i = 1;i <= n;i++){ if(a[i] == 0) continue; step = dist[now] + a[i]; nxt = now + a[i]; if(nxt >= d){ nxt %= d; } if(dist[nxt] > step || dist[nxt] == 0){ dist[nxt] = step; if(vis[nxt] != flag){ vis[nxt] = flag; q.push(nxt); } } } } } int main(){ freopen("math.in","r",stdin); freopen("math.out","w",stdout); while(scanf("%d%d",&n,&d)){ if(!n && !d) break; for(int i = 1;i <= n;i++){ scanf("%d",&a[i]); } spfa(); if(!(dist[0]/d))cout<<1<<endl; else cout<<dist[0]/d<<endl; } return 0; }
No.6 医疗两仪师!
有一颗树,一些点被染成黑色,剩下的被染成白色,要求对节点分组,一个组的节点在树中必须相邻,且一个组中必须有且仅有一个黑点,求分组方案数
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; #define LL long long const int N=300010,mod=1000000007; LL power(LL a,LL b){ if (b==0) return 1; LL x=power(a,b>>1); if (b&1) return (x*x%mod)*a%mod; else return x*x%mod; } LL f[N][2],d[N]; int v[N],q[N],fa[N],vis[N],e[N]; int start[N],next[N<<1],to[N<<1],cnt; inline void connect(int x,int y){ to[++cnt]=y; next[cnt]=start[x]; start[x]=cnt; } int n; int init() { freopen("assaida.in","r",stdin); freopen("assaida.out","w",stdout); scanf("%d",&n); int x; for (int i=2;i<=n;i++){ scanf("%d",&x); x++; connect(x,i); connect(i,x);//建无向图,搜索父亲和遍历儿子的时候会用到 } for (int i=1;i<=n;i++){ scanf("%d",v+i); d[i]=1;//这里d数组的含义应该是:这个节点往下(不包含它)的所有情况的乘积(乘法原理) } return 0; } int work() { int head=0,tail=1,u; q[1]=1; vis[1]=1; while (head<tail){//建树 u=q[++head]; for (int p=start[u];p;p=next[p]){ if (!vis[to[p]]){ q[++tail]=to[p]; vis[to[p]]=1; fa[to[p]]=u; e[to[p]]=e[u]+1; } } } int ret=0; for (int i=tail;i;i--){//自底向上遍历树 int x=q[i]; if (e[x]>ret) ret=e[x];//这一句话应该可以去掉 if (v[x]){//如果我有医疗异能 f[x][1]=d[x];//那么如果我选中了,我可以带上下面没被选中的普通两仪师,也可以自成一队,让下面的两仪师再分组,两种情况加起来,是他子树的所有情况 f[x][0]=0;//我有医疗异能,不可能我自己先不选,再让有医疗异能的团队选上我 }else{//没有异能 f[x][0]=d[x];//那如果我先不选,让上面的团队带我,那么我下面没有选的可以一并选上,选了的就还在以前的团队,同样是所有情况 f[x][1]=0;//如果我选上了 for (int p=start[x];p;p=next[p]){ if (to[p]!=fa[x]){ int v=to[p]; f[x][1]+=(f[v][1]*d[x]%mod)*power(f[v][0]+f[v][1],mod-2);//必须跟上一个团队,跟上这个团队的所有情况,等于他的其他子树的所有情况 } } f[x][1]%=mod; } d[fa[x]]=d[fa[x]]*(f[x][0]+f[x][1])%mod;//向父亲用乘法原理累加上我的所有情况 } cout<<f[1][1]<<endl;//输出真龙被选中后的最大情况数 return 0; } int main() { init(); return work(); }