目录
题目大意:
给定一个数串s和一个数字k,问删除k个数后,数串最小为多少。
求解思路:
删除数字使子串最小的关键在于尽可能删除高位的较大数字。以1432219中删除3个数字为例,删除这一个数字就是从左边的高位1开始比较,当发现后一个数字比前一个数字小的时候我们就需要把前一个数字删除掉,否则就暂时保留这个数字。所以我们在这个测试样例中依次删除4、3、2,得到正确结果1219。
但假设我们从前向后扫描完一次还没删够,单纯按照上面的做法就会出现问题。以14235中删除2个数字进行分析,经过上述的一次从前向后扫描,我们删除一个数字得到子串1235,此时数据升序有序,想要得到最小值,我们就需要对子串进行从后向前的依次删除,直到删够。在这个认识的基础上,我们得到正确答案123。
综上,我们分析出解决该问题的思想关键在于两次扫描,第一次扫描为从前向后进行扫描,当发现后一个数字比前一个数字小的时候就把前一个数字从子列中删除,直到删够k个数或扫描结束。第二次扫描为从后向前的扫描,依次删除最后的数字,直到删够k个(与第一次扫描累计)。
代码实现:
1.采用链表的实现方案:
#include <iostream>
using namespace std;
//声明链表用于存放数据
typedef struct link{
char v;
struct link *next;
struct link *frot;
}num;
//函数,实现q节点向p节点后一位置的插入问题
void point_in(num* &q, num* &p, int x)
{
q=new num;
q->next=NULL;
q->v = x;
p->next = q;
q->frot = p;
p = q;
}
//函数,实现p节点的删除问题
void point_de(num* &p)
{
if(p->frot!=NULL) p->frot->next=p->next;
if(p->next!=NULL) p->next->frot=p->frot;
delete []p;
p=NULL;
}
int main()
{
//本实验用含有空节点的链表实现
//f指针指向链表头部,r指针指向链表的尾部,p、q指针用于对链表中间节点的操纵
num *f = new num;
f->next=NULL; f->frot=NULL;
num *r, *p, *q;
char x;
bool flag=0;
int k, cnt=0;
p = f;
//向链表中插入原始数据
while((x=getchar())!='\n')
{
point_in(q, p, x);
cnt++;
}
r=p;
cin>>k;
cnt=cnt-k;
//当删完链表不剩任何数据时,输出0
if(k<=0)
{
cout<<”k输入异常\n”<<endl;
return 0;
}
if(cnt==0) cout<<0;
else
{
//从前往后删除
for(p=f->next; p!=r&&!flag; p=p->next)
{
while(p->v>p->next->v && !flag)
{
q=p->frot;
point_de(p);
p=q;
if(!--k) flag=1;
if(p = f) break;
}
}
//从后向前删除
while(k)
{
q=r->frot;
point_de(r);
r=q;
k--;
}
//删除开头0元
for(p=f->next; p;)
if(p->v=='0')
{
q=p->next;
point_de(p);
p=q;
if(--cnt==0) break;
}
else break;
//结果输出
if(cnt==0) cout<<0<<endl;
else for(p=f->next; p; p=p->next) cout<<p->v;
}
return 0;
}
2.采用堆栈队列的实现方案:
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
int main()
{
queue <char> q;
stack <char> a, r;
char x;
int k, n;
bool flag=0;
//数据读入
while( (x=getchar()) != '\n')
q.push(x);
n=q.size();
cin>>k;
if(k<=0)
{
cout<<"k输入异常"<<endl;
return 0;
}
if(k==n) cout<<0;
else
{
//从前往后删除并将有效部分压入堆栈a
while(!q.empty()&&k)
{
x=q.front();
q.pop();
if(!a.empty())
{
while(!a.empty()&&x<a.top())
{
a.pop();
if(!--k) break;
}
}
a.push(x);
}
//若删完k个数据且队列q还未删空,则将q中余下数据全部写入堆栈a
while(!q.empty())
{
x=q.front();
q.pop();
a.push(x);
}
//若没有删完k个数则从后往前删
while(k--) a.pop();
//将堆栈a反向转存入堆栈r
while(!a.empty())
{
x=a.top();
a.pop();
r.push(x);
}
根据堆栈r的情况打印出最终的结果
while(!r.empty())
{
if(flag==0)
if(r.top()!='0')
{
cout<<r.top();
flag=1;
}
else cout<<r.top();
r.pop();
}
if(flag==0) cout<<0;
}
return 0;
}
3.采用数组+假删策略的实现方案:
#include <iostream>
using namespace std;
int main()
{
char num[10005]; //存放数串
int f[10005]; //存放该位置的上一个有效位置的下标
int l[10005]; //存放该位置的下一个有效位置的下标
bool effect[10005]={0}; //位置的有效性数组
int i=0, n, k;
char x; bool flag=0;
while( (x=getchar())!='\n' ) num[i++]=x;
n=i;
cin>>k;
if(k<=0)
{
cout<<”k输入异常\n”<<endl;
return 0;
}
if(k==n)
{
cout<<0;
return 0;
}
for(int i=0; i<n; i++)
{
f[i]=i-1;
l[i]=i+1;
}
l[n-1]=-1;
//从前往后扫描删除
for(i=0; i<n-1 && !flag; i++)
{
if(num[i]>num[i+1])
{
effect[i]=1;
f[i+1]=f[i];
l[f[i+1]]=i+1;
if(!--k) flag=1;
for(int j=f[i+1]; j>=0&&!flag; j=f[i+1])
{
if(num[j]<=num[i+1]) break;
effect[j]=1;
f[i+1]=f[j];
l[f[i+1]]=i+1;
if(!--k) flag=1;
}
}
}
//从后往前删除
i=n-1;
while(k--) i=f[i];
l[i]=-1;
//打印结果
flag=0;
for(i=0; i<n; i++)
if(effect[i]==0) break;
for(; l[i]>0; i=l[i])
{
if(!flag)
if(num[i]!='0')
{
cout<<num[i];
flag=1;
}
else cout<<num[i];
}
cout<<num[i]<<endl;
return 0;
}
时间复杂度分析:
首先一次遍历插入数据,时间复杂度为O(n),然后遍历从前往后进行删除,时间复杂度小于O(n),接下来从后往前遍历一次进行二次删除,时间复杂度<O(k),最后遍历输出结果,时间复杂度为O(n-k),因此,总的时间复杂度有:T(n,k)=O(n)+O(n)+O(k)+O(n-k),因此T(n)~O(n)。
方案对比:
链表的实现方式中,对数据插入、删除的操作很容易产生野指针之类的难调试的问题,因此代码的编写上需要耐心调试。而用C++的内置的stack和queue来求解问题,push和pop操作的代码无需自己编写,调试难度相对减小,但是相较链表而言,灵活性又比较差。而相较而言,数组实现不会有以上问题,但是为了保证时间复杂度仍然保持在O(n)的数量级,数组实现时不得不以空间换取时间,多使用4个数组,增加了具体实现时的空间消耗。